Idle Infinity - Filter

Idle Infinity

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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)