Idle Infinity - Filter

Idle Infinity

// ==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)