Idle Infinity - Filter

Idle Infinity

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Idle Infinity - Filter
// @namespace    http://dazzyd.org/
// @version      0.4.7
// @description  Idle Infinity
// @author       Dazzy Ding
// @license      MIT
// @grant        GM_addStyle
// @match        https://www.idleinfinity.cn/Config/Query?*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=idleinfinity.cn
// ==/UserScript==


const store = {
  load() {
    const saved = JSON.parse(localStorage.getItem(`dd_ui_filter`) || "{}")
    for (const [key, val] of Object.entries(saved)) {
      this[key] = val
    }
  },
  save() {
    localStorage.setItem(`dd_ui_filter`, JSON.stringify(this))
  },
}
const validOptions = {}


class Condition {
  constructor(name, value) {
    this.name = name
    this.value = value
  }

  static fromString(string) {
    const matches = string.match(/^【(.+?)】\s*(>=|=|包含)\s*【?(\d+)】?$/)
    if (matches == null) {
      return null
    }
    const cond = new Condition(matches[1], matches[3])
    if (validOptions.prefix.includes(cond.name) || validOptions.skill.includes(cond.name)) {
      return cond
    }
    return null
  }

  toString() {
    if (this.name === "序号") {
      return `【${this.name}】 = ${this.value}`
    }
    if (this.name === "名称") {
      return `【${this.name}】 包含 【${this.value}】`
    }
    return `【${this.name}】 >= ${this.value}`
  }

  compare(other) {
    // 【技能】排在前面,其他按选项顺序
    const indexOf = name =>
      validOptions.prefixAll.indexOf(name) - (name.includes("技能") ? validOptions.prefixAll.length : 0)
    if (this.name !== other.name) {
      return indexOf(this.name) - indexOf(other.name)
    }
    else {
      return this.value - other.value
    }
  }

  isSame(other) {
    return this.compare(other) === 0
  }

  isSkill() {
    return validOptions.skill.includes(this.name)
  }
}

class Rule {
  constructor(power, type, conditions, dom) {
    this.power = power
    this.type = type
    this.conditions = conditions == null ? [] : conditions.sort((a, b) => a.compare(b))
    this.dom = dom
  }

  static fromString(string) {
    const parts = string.split('|')
    const conds = parts.slice(2).map(s => Condition.fromString(s))
    if (conds.indexOf(null) !== -1) {
      return null
    }
    const rule = new Rule(parts[0], parts[1], conds)
    if (validOptions.power.includes(rule.power) && validOptions.type.includes(rule.type)) {
      return rule
    }
    return null
  }

  toString() {
    const parts = []
    parts.push(this.power)
    parts.push(this.type)
    for (const cond of this.conditions) {
      parts.push(cond.toString())
    }
    return parts.join('|')
  }

  compare(other) {
    if (this.power !== other.power) {
      return validOptions.power.indexOf(this.power) - validOptions.power.indexOf(other.power)
    }
    if (this.type !== other.type) {
      return validOptions.type.indexOf(this.type) - validOptions.type.indexOf(other.type)
    }
    if (this.conditions.length !== other.conditions.length) {
      return this.conditions.length - other.conditions.length
    }
    for (const [i, cond] of this.conditions.entries()) {
      const ret = cond.compare(other.conditions[i])
      if (ret !== 0) return ret
    }
    return 0
  }

  isSame(other) {
    return this.compare(other) === 0
  }
}

function createElementByHTML(html) {
  const template = document.createElement('template')
  template.innerHTML = html.trim()
  return template.content.firstChild
}


function addRule(rule) {
  function select_value(dom, value) {
    for (const [index, option] of Object.entries(dom.options)) {
      if (option.text === value) {
        dom.selectedIndex = index
        return
      }
    }
    console.error(`无法在DOM${dom}中找到选项${value}`)
  }

  const modal = document.querySelector("#modalConfig")
  setTimeout(step1, 0)

  function step1() {
    const modalOpen = document.querySelector("a[data-target='#modalConfig']")
    modalOpen.click()
    select_value(modal.querySelector("select#power"), rule.power)
    select_value(modal.querySelector("select#type"), rule.type)
    const condDivList = modal.querySelectorAll("div.condition")
    const condAdd = modal.querySelector("button.config-magic-add")
    for (let i = condDivList.length; i < rule.conditions.length; i++) {
      condAdd.click()
    }
    setTimeout(step2, 250)
  }

  function step2() {
    const condDivList = modal.querySelectorAll("div.condition")
    for (const [index, cond] of rule.conditions.entries()) {
      const div = condDivList[index]
      if (cond.isSkill()) {
        select_value(div.querySelector("select.prefix"), "+ 职业指定技能")
        select_value(div.querySelector("select.sk"), cond.name)
      }
      else {
        select_value(div.querySelector("select.prefix"), cond.name)
      }
      div.querySelector("input.min").value = cond.value
    }
    setTimeout(step3, 250)
  }

  function step3() {
    modal.querySelector("button.config-apply").click()
  }
}

function loadCurrentRules() {
  return Array.from(document.querySelectorAll('tbody tr'))
    .map(row => {
      const tds = row.querySelectorAll('td')
      return new Rule(
        tds[1].innerText,
        tds[2].innerText,
        Array.from(tds[3].querySelectorAll('div.col-sm-6')).map(
          div => Condition.fromString(div.innerText)),
        row,
      )
    })
    .sort((a, b) => a.compare(b))
}

function parseRulesByText(text) {
  const rules = []
  const errors = []
  const lines = text.split('\n').map(s => s.trim()).filter(s => s.length > 0)
  for (const [index, line] of lines.entries()) {
    const rule = Rule.fromString(line)
    if (rule != null) {
      rules.push(rule)
    }
    else {
      errors.push([index, line])
    }
  }
  return [rules, errors]
}

function updateTable() {
  if (store.rules == null) {
    return
  }
  const currentRules = loadCurrentRules()
  const [requireRules, _] = parseRulesByText(store.rules)
  let isExactMatch = true

  // 高亮不在配置中的多余规则
  for (const rule of currentRules) {
    if (requireRules.find(other => rule.isSame(other)) != null) {
      continue
    }
    console.log(`多余规则:${rule}`)
    isExactMatch = false
    rule.dom.style.backgroundColor = "#900"
  }

  // 缺少规则
  let firstCurrent = currentRules[0].dom
  for (const rule of requireRules) {
    if (currentRules.find(other => rule.isSame(other)) != null) {
      continue
    }
    console.log(`缺少规则:${rule}`)
    isExactMatch = false
    const row = createElementByHTML(`
    <tr style="background-color: #009">
      <td class="text-center">无效</td>
      <td class="text-center">${rule.power}</td>
      <td class="text-center">${rule.type}</td>
      <td>
        <div class="container-fluid">
          ${rule.conditions.map(cond => `<div class="col-sm-6 col-md-6"><span>${cond.toString()}</span></div>`).join('')}
        </div>
      </td>
      <td>
        <span class="label label-primary rule-add">添加</span>
      </td>
    </tr>
    `)
    firstCurrent.before(row)
    row.getElementsByClassName('rule-add')[0]
      .addEventListener("click", () => {
        addRule(rule)
      })
  }

  // 如果完全匹配,则清空保存的规则
  if (isExactMatch) {
    store.rules = null
    store.save()
  }
}

function updateUI() {
  document.querySelector(".panel-heading > .pull-right").prepend(createElementByHTML(`
  <a class="btn btn-xs btn-danger" id="helper-open" role="button" data-toggle="modal" data-target="#modalHelper">助手</a>
  `))
  document.getElementById("modalImport").after(createElementByHTML(`
  <div class="modal fade" id="modalHelper" tabindex="-1" role="dialog">
    <div class="modal-dialog modal-md" role="document">
      <div class="modal-content model-inverse">
        <div class="modal-header">
          <span class="modal-title">批量管理助手</span>
        </div>
        <div class="modal-body">
          <div class="form-group">
            <label class="control-label">编辑规则文本并提交</label>
            <textarea class="form-control" rows="20" id="helper-rules"></textarea>
          </div>
          <div class="form-group error" id="helper-error"></div>
          <div class="form-group">
            <label>使用说明</label>
            <p>
              蓝底为需要手动增加,红底为需要手动删除。<br>
              清空规则文本可以停用助手。<br>
            </p>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary btn-xs" id="helper-submit">提交</button>
          <button type="button" class="btn btn-default btn-xs" data-dismiss="modal">关闭</button>
        </div>
      </div>
    </div>
  </div>
  `))

  document.getElementById("helper-open")
    .addEventListener("click", () => {
      const output = store.rules != null ? store.rules
        : loadCurrentRules().map(rule => rule.toString()).join('\n')
      document.getElementById("helper-rules").value = output
    })
  document.getElementById("helper-submit")
    .addEventListener("click", () => {
      const input = document.getElementById("helper-rules").value
      const [_, errors] = parseRulesByText(input)
      if (errors.length > 0) {
        document.getElementById("helper-error").innerHTML =
          errors.map(([index, line]) => `第${index + 1}行:${line}`).join('<br>')
      }
      else {
        store.rules = input.trim() === "" ? null : input
        store.save()
        location.reload()
      }
    })
}

setTimeout(() => {
  store.load()
  for (const [key, selector] of Object.entries({
    power: "#modalConfig #power option",
    type: "#modalConfig #type option",
    prefix: ".condition-template .prefix option",
    skill: ".condition-template .sk option",
  })) {
    validOptions[key] = Array.from(document.querySelectorAll(selector)).map(
      e => e.innerText.trim())
  }
  validOptions.prefixAll = [].concat(validOptions.prefix, validOptions.skill)

  updateTable()
  updateUI()
}, 0)