wsmud_api

使用于 Tampermonkey 的武神传说脚本的前置 API 库

目前為 2020-08-27 提交的版本,檢視 最新版本

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         wsmud_api
// @namespace    com.wsmud
// @version      0.0.7
// @description  使用于 Tampermonkey 的武神传说脚本的前置 API 库
// @author       sq
// @date         2020/08/24
// @modified     2020/08/28
// @match        http://*.wsmud.com/*
// @exclude      http://*.wsmud.com/news/*
// @exclude      http://*.wsmud.com/pay.html
// @homepage     https://greasyfork.org/zh-CN/scripts/409901
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js
// @run-at       document-start
// @grant        unsafeWindow
// @grant        GM_addStyle
// ==/UserScript==

(function() {

'use strict'

if (!unsafeWindow.WebSocket) return

// GM_info.script.version

unsafeWindow.console.green = function(log) {
  console.log(`%c${log}`, 'color:green')
}
unsafeWindow.console.orange = function(log) {
  console.log(`%c${log}`, 'color:orange')
}
unsafeWindow.console.red = function(log) {
  console.log(`%c${log}`, 'color:red')
}
console.green = unsafeWindow.console.green
console.orange = unsafeWindow.console.orange
console.red = unsafeWindow.console.red

class Api {
  constructor() {
    this.roles = this.getValue('roles')
    this.id = String()
    this.name = String()
    this.state = String()
    this.performs = Array()
    this.room = Object()

    this.monitors = Object()
    this.commandQueue = Array()
    this.commandState = false
  }
  set roles(value) {
    if (value instanceof Array) {
      value.sort((a, b) => a.sort - b.sort)
      value.forEach((item, index) => {
        if (item.server) item.sort = index + 1
        else item.sort = 9999
      })
      this._roles = value
    } else {
      this._roles = []
    }
    this.setValue('roles', this._roles)
  }
  get roles() {
    return this._roles
  }

  ondata(data) {
    const type = data.type === 'dialog' ? data.dialog : data.type
    if (this.monitors[type]) {
      Object.keys(this.monitors[type]).forEach(name => {
        const callback = this.monitors[type][name]
        callback(data)
      })
    }
    const data2event = function(data) {
      if (!data.type) return
      if (data.type === 'text') return { data: data.text }
      else return { data: JSON.stringify(data) }
    }
    const event = data2event(data)
    if (event) this._onmessage(event)
  }
  send(...args) {
    // 分割含有英文逗号的字符串
    args.forEach((item, index) => {
      if (typeof item === 'string' && /,/.test(item) && /^(?!setting)/.test(item)) args[index] = item.split(',')
    })
    // 加入指令队列
    this.commandQueue.push(...args.flat(Infinity))
    // 开启消息队列
    if (!this.commandState) {
      this.commandState = true
      this.sendloop(0)
    }
  }
  sendloop(delay = 256) {
    if (!this._websocket || !this.commandState || this.commandQueue.length === 0) {
      this.commandState = false
      return
    }
    // 取出首位元素
    const cmd = this.commandQueue.splice(0, 1)[0]
    // 判断是否数字
    const number = Number(cmd)
    if (!isNaN(number)) {
      console.orange(`CommandQueue: Wait for ${number}ms.`)
      this.sendloop(number)
      return
    }
    // 判断是否字符串
    if (typeof cmd === 'string') {
      setTimeout(() => {
        console.orange(cmd)
        this._websocket.send(cmd)
        this.sendloop()
      }, delay)
      return
    }
    // 判断是否函数
    if (typeof cmd === 'function') {
      setTimeout(() => {
        cmd()
        this.sendloop()
      }, delay)
      return
    }
  }
  clearCommandQueue() {
    this.commandQueue.splice(0)
  }

  addMonitor(type, name, callback) {
    if (!type || !name || typeof callback !== 'function') return
    if (!this.monitors[type]) this.monitors[type] = {}
    this.monitors[type][name] = callback.bind(this)
    console.green(`AddMonitor: [${type}]${name}`)
  }
  removeMonitor(type, name) {
    if (!type || !name) return
    delete this.monitors[type][name]
    console.red(`RemoveMonitor: type = ${type}; name = ${name};`)
    api.roles = new Object(api.roles)
  }
  addStyle(css) {
    GM_addStyle(css)
  }
  cookie() {
    const cookies = document.cookie.split(';').reduce((accumulator, currentValue) => {
      const i = currentValue.indexOf('=')
      const name = currentValue.substr(0, i).trim()
      const value = currentValue.substr(i + 1)
      accumulator[name] = value
      return accumulator
    }, {})
    const setCookie = (name, value) => document.cookie = name + '=' + value
    const deleteCookie = name => document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
    return new Proxy(cookies, {
      set: (target, name, value) => {
        setCookie(name, value)
        return Reflect.set(target, name, value)
      },
      deleteProperty: (target, name) => {
        deleteCookie(name)
        return Reflect.deleteProperty(target, name)
      },
    })
  }
  setValue(key, value) {
    localStorage.setItem(key, JSON.stringify(value))
  }
  getValue(key) {
    return JSON.parse(localStorage.getItem(key))
  }
  deleteValue(key) {
    localStorage.removeItem(key)
  }
}

unsafeWindow.Vue = Vue
unsafeWindow.api = Vue.observable(new Api())
// const api = unsafeWindow.api

unsafeWindow.WebSocket = function (uri) {
  api._websocket = new WebSocket(uri)
}
unsafeWindow.WebSocket.prototype = {
  set onopen(fn) {
    api._websocket.onopen = fn
  },
  set onclose(fn) {
    api._websocket.onclose = fn
  },
  set onerror(fn) {
    api._websocket.onerror = fn
  },
  set onmessage(fn) {
    const event2data = function(event) {
      const data = event.data
      if (typeof data === 'string' && data[0] === '{') {
        try {
          return new Function('return ' + data)()
        } catch (error) {
          console.red(error)
          console.red(data)
        }
      }
      return { 'type': 'text', 'text': data }
    }
    api._onmessage = fn
    api._websocket.onmessage = function(event) {
      const data = event2data(event)
      api.ondata(data)
    }
  },
  get readyState() {
    return api._websocket.readyState
  },
  send: function(command) {
    api.send(command)
  },
}

api.addMonitor('roles', 'RoleList', function(data) {
  if (!(data.roles instanceof Array)) return
  data.roles.forEach(item => {
    const { id, name, title } = item
    const index = this.roles.findIndex(role => role.id === id)
    if (index === -1) {
      this.roles.push({ id, name, title, sort: 9999 })
    } else {
      this.roles[index].name = name
      this.roles[index].title = title
    }
  })
  this.roles = this.roles.slice(0)
})
api.addMonitor('login', 'Login', function(data) {
  const id = data.id
  if (this.id || !id) return
  this.id = id
  const index = this.roles.findIndex(role => role.id === id)
  this.name = this.roles[index].name
  const cookie = this.cookie()
  this.roles[index].u = cookie.u
  this.roles[index].p = cookie.p
  this.roles[index].s = cookie.s
  this.roles[index].server = ['一区', '二区', '三区', '四区', '测试'][cookie.s]
  this.roles = this.roles.slice(0)

  setTimeout(() => {
    this.send('pack,score2,score')
    setTimeout(() => $('[command=skills]').click(), 1000)
    setTimeout(() => $('[command=tasks]').click(), 2000)
    setTimeout(() => $('.dialog-close').click(), 3000)
    if (!$('.right-bar').width()) {
      setTimeout(() => $('[command=showtool]').click(), 500)
    }
    if (!$('.content-bottom').height()) {
      setTimeout(() => $('[command=showcombat]').click(), 1000)
    }
  }, 2000)
  // greet master 请安师傅
})
api.addMonitor('state', 'State', function(data) {
  if (typeof data.state !== 'string') {
    this.state = ''
    return
  }
  const list = [
    { regexp: /^你正在挖矿中$/, value: '挖矿' },
    { regexp: /^你正在练习技能$/, value: '练习' },
    { regexp: /^你正在学习(\S+)$/, value: '学习' },
    { regexp: /^你正在打坐运功$/, value: '打坐' },
    { regexp: /^你正在疗伤$/, value: '疗伤' },
    { regexp: /^你正在闭关$/, value: '闭关' },
    { regexp: /^你正在炼药$/, value: '炼药' },
    { regexp: /^你正在读书$/, value: '读书' },
    { regexp: /^你正在领悟技能$/, value: '武道领悟' },
  ]
  const result = list.find(item => item.regexp.test(data.state))
  if (result) {
    data.state = result.value + (RegExp.$1 || '')
  } else {
    console.red(data.state)
  }
  this.state = data.state
  delete data.desc
})
api.addMonitor('combat', 'Fight', function(data) {
  if (data.start === 1) {
    this.state = '战斗'
    // if (this.fightTimer) clearInterval(this.fightTimer)
    // this.fightTimer = setInterval(() => {
    //   const now = Date.now()
    //   this.performs.find(pfm => {
    //     if (pfm.auto && (pfm.time < now)) {
    //       this.send(`perform ${pfm.id}`)
    //       return true
    //     }
    //   })
    // }, 256)
  } else if (data.end === 1) {
    this.state = ''
    // clearInterval(this.fightTimer)
  }
})
api.addMonitor('die', 'Die', function(data) {
  if (data.relive) {
    this.state = ''
  } else {
    this.state = '死亡'
    // // 自动武庙复活
    // if (this.CanReliveWuMiao && !this.roomName.includes('副本')) {
    // this.send('relive')
  }
})

class Perform {
  constructor(item) {
    this.id = item.id
    this.name = item.name
    this.distime = item.distime
    this.timestamp = 0
    this.auto = api.getValue(this.key('auto')) || false
    this.sort = api.getValue(this.key('sort')) || 999
  }
  key(value) {
    return `${api.id}-${this.id}-${value}`
  }
  set auto(value) {
    this._auto = Boolean(value)
    api.setValue(this.key('auto'), this._auto)
  }
  get auto() {
    return this._auto
  }
  set sort(value) {
    this._sort = Number(value)
    api.setValue(this.key('sort'), this._auto)
  }
  get sort() {
    return this._sort
  }
}
api.addMonitor('perform', 'PerformList', function(data) {
  if (!(data.skills instanceof Array)) return
  data.skills.forEach((item, index) => data.skills[index] = new Perform(item))
  data.skills.forEach((a, b) => a.sort - b.sort)
  data.skills.forEach((item, index) => item.sort = index + 1)
  api.performs = data.skills
})
api.addMonitor('dispfm', 'PerformDistime', function(data) {
  if (data.hasOwnProperty('id') && data.hasOwnProperty('distime')) {
    api.performs.forEach(pfm => {
      if (pfm.id === data.id) {
        pfm.distime = data.distime
        pfm.timestamp = Date.now() + data.distime
      }
      if (data.hasOwnProperty('rtime')) {
        pfm.time = Math.max(Date.now() + data.rtime, pfm.timestamp)
      }
    })
  }
})

class Room {
  constructor(item) {
    this.name = item.name
    this.path = item.path
    this.commands = item.commands || Array()
    this.desc = item.desc
    this.exits = Object()
    this.items = Array()
  }
  get x() {
    if (/^(\S+)-(\S+)$/.test(this.name)) {
      return RegExp.$1
    }
  }
  get y() {
    if (/^(\S+)-(\S+)\(副本区域\)$/.test(this.name)) {
      return RegExp.$2
    } else if (/^(\S+)-(\S+)$/.test(this.name)) {
      return RegExp.$2
    }
  }
  set desc(value) {
    if (/cmd/.test(value)) {
      // 统一使用双引号
      value = value.replace(/'/g, '"')
      // 去除英文括号和里面的英文单词
      value = value.replace(/\([A-Za-z]+?\)/g, '')
      // 新手教程中的椅子
      value = value.replace(/<hig>椅子<\/hig>/, '椅子')
      // 兵营副本中的门
      value = value.replace(/<CMD cmd="look men">门<CMD>/, '<cmd cmd="look men">门</cmd>');
      // 古墓副本中的画和古琴
      value = value.replace(/span/ig, 'cmd')

      const htmls = value.match(/<cmd cmd="[^"]+?">[^<]+?<\/cmd>/g)
      htmls.forEach(html => {
        if (/<cmd cmd="([^"]+?)">([^<]+?)<\/cmd>/.test(html)) {
          this.commands.unshift({ cmd: RegExp.$1, name: `<hic>${RegExp.$2}</hic>` })
        }
      })
    }
    this._desc = value
  }
  get desc() {
    return this._desc
  }
  get npcs() {
    return this.items.filter(item => item.isNpc)
  }
}
api.addMonitor('room', 'RoomInfo', function(data) {
  api.room = new Room(data)
  data.desc = api.room.desc
  data.commands = api.room.commands
})
api.addMonitor('exits', 'RoomExitInfo', function(data) {
  if (typeof data.items !== 'object') return
  api.room.exits = {}
  Object.keys(data.items).forEach(key => api.room.exits[`go ${key}`] = data.items[key])
})

class Role {
  constructor(data) {
    this.id = data.id
    this.name = data.name
    if (data.hasOwnProperty('hp')) this.hp = data.hp
    if (data.hasOwnProperty('mp')) this.mp = data.mp
    if (data.hasOwnProperty('max_hp')) this.max_hp = data.max_hp
    if (data.hasOwnProperty('max_mp')) this.max_mp = data.max_mp
    if (data.hasOwnProperty('status')) this.status = data.status
    if (data.hasOwnProperty('p')) this.p = data.p // 玩家肯定有 p === 1
  }
  get isNpc() {
    return !this.hasOwnProperty('p') && this.hasOwnProperty('hp')
  }
  get sort() {
    // 自身
    if (this.id === api.id) return 1
    // NPC
    if (this.isNpc) return 10
    // 玩家
    if (this.hasOwnProperty('hp')) {
      const list = ['武神 ', '武帝 ', '武圣 ', '宗师 ', '武师 ', '武士 ', '普通百姓 ', '']
      const index = list.findIndex(item => this.name.includes(item))
      return 100 * index * (this.name.includes('<red>&lt;断线中&gt;</red>') ? 100 : 1)
    }
    // 其他物品或尸体
    return 1000
  }
}
api.addMonitor('items', 'RoomItemList', function(data) {
  if (!data.items instanceof Array) return
  data.items.splice(-1, 1) // 删除最后一个 0
  data.items.forEach((item, index) => data.items[index] = new Role(item))
  data.items.sort((a, b) => a.sort - b.sort)
  api.room.items = data.items
})
api.addMonitor('itemadd', 'RoomAddItem', function(data) {
  const item = new Role(data)
  const index = api.room.items.findIndex(item => item.id === data.id)
  if (index === -1) api.room.items.push(item)
  else api.room.items.splice(index, 1, item)
})
api.addMonitor('itemremove', 'RoomRemoveItem', function(data) {
  const index = api.room.items.findIndex(item => item.id === data.id)
  if (index !== -1) api.room.items.splice(index, 1)
})

api.addMonitor('', '', function(data) {})
api.addMonitor('', '', function(data) {})

class Skill {
  constructor(item) {
    this.id = item.id
    this.name = item.name
    this.level = item.level
    this.target = api.getValue(this.key('target'))
  }
  key(value) {
    return `${api.id}-${this.id}-${value}`
  }
  set target(value) {
    const number = Number(value)
    this._target = isNaN(number) ? 0 : number
    api.setValue(this.key('target'), this._target)
  }
  get target() {
    return this._target
  }
}

new Vue({
  computed: {
    title() {
      return `${api.name} ${api.state}`
    },
  },
  watch: {
    title(value) {
      document.title = value
    },
  },
})

document.addEventListener('DOMContentLoaded', () => api.addStyle(`
`), false)

})()