event.js

'use strict'

// ******************************** 回调管理器 ********************************

// 在移除场景对象的组件时,经常需要将操作推迟到栈尾
// 比如在遍历组件列表时调用了事件或脚本,将其中一个组件移除
// 就会影响到正在遍历的过程,从而产生意外
// 可以用Callback.push(fn)将要做的事情推迟到当前帧的栈尾执行
const Callback = new class {
  functions = []
  count = 0

  /**
   * 推送回调函数,稍后执行
   * @param {Function} fn 回调函数
   */
  push(fn) {
    this.functions[this.count++] = fn
  }

  /** 执行回调函数 */
  update() {
    for (let i = 0; i < this.count; i++) {
      this.functions[i]()
      this.functions[i] = null
    }
    this.count = 0
  }

  /** 重置回调堆栈 */
  reset() {
    for (let i = 0; i < this.count; i++) {
      this.functions[i] = null
    }
    this.count = 0
  }
}

// ******************************** 全局事件管理器 ********************************

const EventManager = new class {
  // 管理器版本号(重置时更新)
  version = 0

  // 全局事件映射表(GUID->指令列表)
  guidMap = {}

  // 特殊事件映射表
  special = {}

  // 事件类型映射表(类型->事件列表)
  typeMap = {
    common: [],
    autorun: [],
    keydown: [],
    keyup: [],
    mousedown: [],
    mouseup: [],
    mousemove: [],
    doubleclick: [],
    wheel: [],
    gamepadbuttonpress: [],
    gamepadbuttonrelease: [],
    gamepadleftstickchange: [],
    gamepadrightstickchange: [],
    skilladd: [],
    skillremove: [],
    stateadd: [],
    stateremove: [],
    equipmentadd: [],
    equipmentremove: [],
    equipmentgain: [],
    itemgain: [],
    moneygain: [],
  }

  // 已激活事件列表
  activeEvents = []

  /** 初始化全局事件管理器 */
  initialize() {
    const {guidMap, typeMap} = this
    const events = Object.values(Data.events)

    // 删除数据释放内存
    delete Data.events

    // 编译事件指令
    for (const {id, path, enabled, priority, type, commands} of events) {
      commands.path = '@ ' + path
      const cmds = Command.compile(commands)
      let parent = typeMap[type]
      if (parent === undefined) {
        parent = typeMap[type] = []
      }
      cmds.default = enabled
      cmds.enabled = enabled
      cmds.priority = priority
      cmds.parent = parent
      parent.push(cmds)
      guidMap[id] = cmds
    }

    // 获取特殊事件
    this.special.startup = guidMap[Data.config.event.startup]
    this.special.loadGame = guidMap[Data.config.event.loadGame]
    this.special.initScene = guidMap[Data.config.event.initScene]
    this.special.showText = guidMap[Data.config.event.showText]
    this.special.showChoices = guidMap[Data.config.event.showChoices]

    // 侦听事件
    Scene.on('initialize', () => this.callSpecialEvent('initScene'))
    Scene.on('keydown', () => this.emit('keydown', false))
    Scene.on('keyup', () => this.emit('keyup', false))
    Scene.on('mousedown', () => this.emit('mousedown', false))
    Scene.on('mouseup', () => this.emit('mouseup', false))
    Scene.on('mousemove', () => this.emit('mousemove', false))
    Scene.on('doubleclick', () => this.emit('doubleclick', false))
    Scene.on('wheel', () => this.emit('wheel', false))
    Scene.on('gamepadbuttonpress', () => this.emit('gamepadbuttonpress', false))
    Scene.on('gamepadbuttonrelease', () => this.emit('gamepadbuttonrelease', false))
    Scene.on('gamepadleftstickchange', () => this.emit('gamepadleftstickchange', false))
    Scene.on('gamepadrightstickchange', () => this.emit('gamepadrightstickchange', false))
    Input.on('keydown', () => this.emit('keydown', true), true)
    Input.on('keyup', () => this.emit('keyup', true), true)
    Input.on('mousedown', () => this.emit('mousedown', true), true)
    Input.on('mouseup', () => this.emit('mouseup', true), true)
    Input.on('mousemove', () => this.emit('mousemove', true), true)
    Input.on('doubleclick', () => this.emit('doubleclick', true), true)
    Input.on('wheel', () => this.emit('wheel', true), true)
    Input.on('gamepadbuttonpress', () => this.emit('gamepadbuttonpress', true), true)
    Input.on('gamepadbuttonrelease', () => this.emit('gamepadbuttonrelease', true), true)
    Input.on('gamepadleftstickchange', () => this.emit('gamepadleftstickchange', true), true)
    Input.on('gamepadrightstickchange', () => this.emit('gamepadrightstickchange', true), true)
  }

  /**
   * 获取指定ID的事件指令
   * @param {string} id 事件ID
   * @returns {Array<Function>}
   */
  get(id) {
    return this.guidMap[id]
  }

  /** 重置全局事件的开关状态 */
  reset() {
    for (const commands of Object.values(this.guidMap)) {
      commands.enabled = commands.default
    }
    this.version++
  }

  /** 调用特殊事件 */
  callSpecialEvent(type) {
    const commands = this.special[type]
    if (commands) {
      EventHandler.call(new EventHandler(commands))
    }
  }

  /** 调用自动执行事件 */
  callAutorunEvents() {
    for (const commands of this.typeMap.autorun) {
      EventHandler.call(new EventHandler(commands))
    }
  }

  /**
   * 调用全局事件
   * @param {string} id 全局事件文件ID
   * @returns {EventHandler|undefined}
   */
  call(id) {
    const commands = this.guidMap[id]
    if (commands) {
      const event = new EventHandler(commands)
      EventHandler.call(event, this.updaters)
      return event
    }
  }

  /**
   * 发送全局事件
   * @param {string} type 全局事件类型
   * @param {boolean} priority 是不是优先事件
   * @param {Object} [options] 传递事件上下文属性
   */
  emit(type, priority = null, options) {
    for (const commands of this.typeMap[type] ?? []) {
      if (commands.enabled && (priority === null ||
        commands.priority === priority)) {
        const event = new EventHandler(commands)
        // 添加传递的数据到事件上下文
        if (options) Object.assign(event, options)
        // 设置事件优先级
        event.priority = commands.priority
        EventHandler.call(event)
        // 如果事件停止传递,跳出
        if (Input.bubbles.get() === false) {
          break
        }
      }
    }
  }

  /**
   * 添加已激活事件处理器
   * @param {EventHandler} event 事件处理器
   */
  append(event) {
    this.activeEvents.push(event)
    // 添加事件完成回调函数:延迟移除
    event.onFinish(() => {
      Callback.push(() => {
        this.activeEvents.remove(event)
      })
    })
  }

  /**
   * 更新管理器中的已激活事件处理器
   * @param {number} deltaTime 增量时间(毫秒)
   */
  update(deltaTime) {
    if (Scene.paused === 0) {
      for (const event of this.activeEvents) {
        event.update(deltaTime)
      }
    } else {
      for (const event of this.activeEvents) {
        if (event.priority) {
          event.update(deltaTime)
        }
      }
    }
  }

  /**
   * 启用全局事件(延迟)
   * @param {string} id 全局事件文件ID
   */
  enable(id) {
    const commands = this.guidMap[id]
    if (commands) {
      const {version} = this
      commands.callback = () => {
        if (this.version === version) {
          commands.enabled = true
        }
        commands.callback = null
      }
      Callback.push(() => {
        commands.callback?.()
      })
    }
  }

  /**
   * 禁用全局事件(立即)
   * @param {string} id 全局事件文件ID
   */
  disable(id) {
    const commands = this.guidMap[id]
    if (commands) {
      commands.enabled = false
      commands.callback = null
    }
  }

  /**
   * 设置全局事件为最高优先级
   * @param {string} id 全局事件文件ID
   */
  setToHighestPriority(id) {
    const commands = this.guidMap[id]
    if (commands) {
      // 延迟执行,将事件移动到头部
      Callback.push(() => {
        commands.priority = true
        const list = commands.parent
        const index = list.indexOf(commands)
        for (let i = index; i > 0; i--) {
          list[i] = list[i - 1]
        }
        list[0] = commands
      })
    }
  }
}

// ******************************** 插件管理器 ********************************

const PluginManager = new class {
  /** 初始化插件管理器 */
  initialize() {
    const {plugins} = Data
    // 删除初始化方法和插件数据
    delete this.initialize
    delete Data.plugins
    const manager = Script.create({}, plugins)
    // 获取脚本实例,以类名作为键进行注册
    for (const instance of manager.instances) {
      const {name} = instance.constructor
      if (name !== '') this[name] = instance
    }
    // 发送自动执行事件(onStart)
    manager.emit('autorun')
  }
}

// 正在执行的事件相关属性(全局)
let Event = {commands: [], index: 0}
let CommandList
let CommandIndex

// ******************************** 事件处理器类 ********************************

class EventHandler {
  complete    //:boolean
  commands    //:array
  index       //:number
  stack       //:array
  attributes  //:object
  timer       //:object

  /**
   * 事件处理器
   * @param {function[]} commands 事件指令列表
   */
  constructor(commands) {
    this.complete = false
    this.commands = commands
    this.index = 0
    this.stack = new CommandStack()
    this.attributes = {}
  }

  /**
   * 执行事件指令
   * @returns {boolean} 事件已完成状态
   */
  update() {
    // 设置相关属性到全局变量
    Event = this
    CommandList = this.commands
    CommandIndex = this.index
    // 连续执行指令,直到返回false(中断)
    while (CommandList[CommandIndex++]()) {}
    // 取回全局变量中的事件属性
    this.commands = CommandList
    this.index = CommandIndex
    // 返回事件完成状态
    return this.complete
  }

  /**
   * 获取事件计时器(计时器会替换update方法,用于事件指令异步等待)
   * @returns {{set: function, continue: function}} 事件计时器
   */
  getTimer() {
    let timer = this.timer
    if (timer === undefined) {
      // 事件首次等待按需创建计时器
      let duration = 0
      const update = this.update
      const tick = deltaTime => {
        return (duration -= deltaTime) <= 0
        ? (this.update = update, this.update())
        : false
      }
      // 在闭包中创建计时器
      timer = this.timer = {
        set: waitingTime => {
          duration = waitingTime
          // 设置更新函数为:计时
          this.update = tick
        },
        continue: () => {
          // 恢复更新函数
          this.update = update
        },
        get duration() {
          return duration
        },
        set duration(value) {
          duration = value
        },
      }
    }
    return timer
  }

  /**
   * 事件等待指定时间
   * @param {number} duration 等待时间(毫秒)
   * @returns {false} 中断指令的执行
   */
  wait(duration) {
    this.getTimer().set(duration)
    return false
  }

  /**
   * 暂停执行事件
   * @returns {false} 中断指令的执行
   */
  pause() {
    this.getTimer()
    // 设置更新函数为:等待
    this.update = EventHandler.wait
    return false
  }

  /** 继续执行事件 */
  continue() {
    this.timer.continue()
  }

  /** 调用事件结束回调函数 */
  finish() {
    this.complete = true
    // 执行结束回调
    if (this.callbacks !== undefined) {
      for (const callback of this.callbacks) {
        callback()
      }
    }
  }

  /**
   * 设置事件结束回调
   * @param {function} callback 回调函数
   */
  onFinish(callback) {
    if (this.complete) {
      callback()
    } else {
      // 添加回调函数到队列中
      if (this.callbacks !== undefined) {
        this.callbacks.push(callback)
      } else {
        this.callbacks = [callback]
      }
    }
  }

  // 继承事件上下文
  inheritEventContext(event) {
    this.attributes = event.attributes
    if ('priority' in event) {
      this.priority = event.priority
    }
    for (const key of EventHandler.inheritedKeys) {
      // 继承事件上下文属性
      if (key in event) {
        this[key] = event[key]
      }
    }
  }

  /**
   * 返回等待状态(暂停事件方法)
   * @returns {function}
   */
  static wait = () => false

  /**
   * 调用事件
   * @param {EventHandler} event 事件处理器
   * @param {ModuleList} updaters 更新器列表
   * @returns {EventHandler} 传入的事件处理器
   */
  static call = (event, updaters) => {
    this.save()
    // 如果事件更新后发生了等待
    if (event.update() === false) {
      if (updaters !== undefined) {
        // 如果指定了更新器列表,延迟将未执行完的事件放入
        Callback.push(() => {
          updaters.append(event)
        })
        // 设置事件结束时回调函数:延迟从更新器中移除
        event.onFinish(() => {
          Callback.push(() => {
            updaters.remove(event)
          })
        })
      } else {
        // 如果未指定更新器列表,添加到事件管理器中
        Callback.push(() => {
          EventManager.append(event)
        })
      }
    }
    this.restore()
    return event
  }

  // 事件栈
  static stacks = []

  // 事件栈索引
  static index = 0

  /** 保存正在执行的事件状态 */
  static save() {
    this.stacks[this.index++] = Event
    Event.commands = CommandList
    Event.index = CommandIndex
  }

  /** 恢复事件状态 */
  static restore() {
    Event = this.stacks[--this.index]
    CommandList = Event.commands
    CommandIndex = Event.index
  }

  // 继承事件上下文的属性键
  static inheritedKeys = [
    'triggerActor',
    'casterActor',
    'triggerSkill',
    'triggerState',
    'triggerEquipment',
    'triggerItem',
    'triggerObject',
    'triggerLight',
    'triggerRegion',
    'triggerElement',
  ]
}

// ******************************** 脚本管理器类 ********************************

class Script {
  parent    //:object
  instances //:array

  /**
   * 脚本管理器
   * @param {Object} owner 脚本宿主对象
   */
  constructor(owner) {
    this.parent = owner
    this.instances = []
  }

  /**
   * 添加脚本对象
   * @param {Object} instance 脚本对象
   */
  add(instance) {
    // 以脚本类名作为键进行注册
    const name = instance.constructor.name
    if (name !== '') this[name] = instance
    // 如果实现了update方法,则添加到父级更新器列表
    if (typeof instance.update === 'function') {
      this.parent.updaters?.push(instance)
    }
    instance.onScriptAdd?.(this.parent)
    this.instances.push(instance)
  }

  /**
   * 移除脚本对象(未使用)
   * @param {Object} instance 脚本对象
   */
  remove(instance) {
    const name = instance.constructor.name
    if (this[name] === instance) delete this[name]
    if (typeof instance.update === 'function') {
      this.parent.updaters?.remove(instance)
    }
    instance.onScriptRemove?.(this.parent)
    this.instances.remove(instance)
  }

  /**
   * 调用脚本方法
   * @param {string} method 方法名称
   * @param  {...any} parameters 传递参数
   */
  call(method, ...parameters) {
    for (const instance of this.instances) {
      instance[method]?.(...parameters)
    }
  }

  /**
   * 发送脚本事件
   * @param {string} type 事件类型
   * @param {any} parameter 传递参数
   */
  emit(type, parameter) {
    // 将事件类型映射到脚本事件方法名称
    const method = Script.eventTypeMap[type]
    // 调用每个脚本对象的事件方法,并传递参数
    for (const instance of this.instances) {
      instance[method]?.(parameter)
    }
  }

  // 延迟加载函数参数开关
  // 速度比调用闭包函数快一点
  static deferredLoading = false
  static deferredCount = 0
  static deferredInstances = []
  static deferredKeys = []
  static deferredValues = []

  /**
   * 放入延迟获取的脚本参数
   * 等待场景对象和UI元素创建完毕后再获取
   * @param {Object} instance 脚本对象
   * @param {string} key 
   * @param {function} value 
   */
  static pushDeferredParameter(instance, key, value) {
    Script.deferredInstances[Script.deferredCount] = instance
    Script.deferredKeys[Script.deferredCount] = key
    Script.deferredValues[Script.deferredCount] = value
    Script.deferredCount++
  }

  /** 加载延迟参数到脚本对象中 */
  static loadDeferredParameters() {
    for (let i = 0; i < Script.deferredCount; i++) {
      Script.deferredInstances[i][Script.deferredKeys[i]] = Script.deferredValues[i]()
      Script.deferredInstances[i] = null
      Script.deferredValues[i] = null
    }
    Script.deferredCount = 0
    Script.deferredLoading = false
  }

  /**
   * 创建脚本管理器(使用脚本数据)
   * @param {Object} owner 脚本宿主对象
   * @param {Object[]} data 脚本数据列表
   * @returns {Script}
   */
  static create(owner, data) {
    const manager = new Script(owner)
    // 如果脚本列表不为空
    if (data.length > 0) {
      for (const wrap of data) {
        // 如果脚本已禁用,跳过
        if (wrap.enabled === false) continue
        // 初始化以及重构参数列表(丢弃无效参数)
        if (wrap.initialized === undefined) {
          wrap.initialized = true
          wrap.parameters = Script.compileParamList(wrap.id, wrap.parameters)
        }
        const {id, parameters} = wrap
        const script = Data.scripts[id]
        // 如果不存在脚本,发送警告
        if (script === undefined) {
          const meta = Data.manifest.guidMap[id]
          const name = meta?.path ?? `#${id}`
          console.error(new Error(`The script is missing: ${name}`), owner)
          continue
        }
        // 创建脚本对象实例,并传递脚本参数
        const instance = new script.constructor(owner)
        const length = parameters.length
        for (let i = 0; i < length; i += 2) {
          const key = parameters[i]
          let value = parameters[i + 1]
          if (typeof value === 'function') {
            if (Script.deferredLoading) {
              // 如果值类型是函数,且开启了延时加载参数开关
              Script.pushDeferredParameter(instance, key, value)
              continue
            }
            value = value()
          }
          instance[key] = value
        }
        manager.add(instance)
      }
    }
    return manager
  }

  /**
   * 编译脚本参数列表
   * @param {string} id 脚本文件ID
   * @param {Object[]} parameters 脚本参数数据列表
   * @returns {Array} 编译后的脚本参数列表
   */
  static compileParamList(id, parameters) {
    const script = Data.scripts[id]
    // 如果不存在脚本,返回空列表
    if (script === undefined) {
      return Array.empty
    }
    const defParameters = script.parameters
    const length = defParameters.length
    // 如果不存在参数,返回空列表
    if (length === 0) {
      return Array.empty
    }
    // 创建扁平化的参数列表
    const parameterList = new Array(length * 2)
    for (let i = 0; i < length; i++) {
      const defParameter = defParameters[i]
      const {key, type} = defParameter
      let value = parameters[key]
      // 根据默认参数类型,对实参进行有效性检查
      // 如果实参是无效的,则使用默认值
      switch (type) {
        case 'boolean':
        case 'number':
          if (typeof value !== type) {
            value = defParameter.value
          }
          break
        case 'variable-number':
          if (typeof value !== 'number') {
            if (value?.getter === 'variable') {
              value = Command.compileVariable(value, Attribute.NUMBER_GET)
            } else {
              value = () => undefined
            }
          }
          break
        case 'option':
          if (!defParameter.options.includes(value)) {
            value = defParameter.value
          }
          break
        case 'number[]':
        case 'string[]':
          if (Array.isArray(value)) {} else {
            value = defParameter.value
          }
          break
        case 'attribute':
          value = Attribute.get(value)
          break
        case 'attribute-key':
          value = Attribute.getKey(value)
          break
        case 'enum':
          value = Enum.get(value)
          break
        case 'enum-value':
          value = Enum.getValue(value)
          break
        case 'actor': {
          const id = value
          value = () => Scene.idMap[id]
          break
        }
        case 'region': {
          const id = value
          value = () => Scene.idMap[id]
          break
        }
        case 'light': {
          const id = value
          value = () => Scene.idMap[id]
          break
        }
        case 'animation': {
          const id = value
          value = () => Scene.idMap[id]
          break
        }
        case 'particle': {
          const id = value
          value = () => Scene.idMap[id]
          break
        }
        case 'parallax': {
          const id = value
          value = () => Scene.idMap[id]
          break
        }
        case 'tilemap': {
          const id = value
          value = () => Scene.idMap[id]
          break
        }
        case 'element': {
          const id = value
          value = () => UI.idMap[id]
          break
        }
        case 'keycode':
          if (typeof value !== 'string') {
            value = defParameter.value
          }
          break
        case 'variable-getter':
          if (value?.getter === 'variable') {
            value = {
              get: Command.compileVariable(value, Attribute.GET),
              set: Command.compileVariable(value, Attribute.SAFE_SET),
            }
          } else {
            value = () => undefined
          }
          break
        case 'actor-getter':
          if (value?.getter === 'actor') {
            value = Command.compileActor(value)
          } else {
            value = () => undefined
          }
          break
        case 'skill-getter':
          if (value?.getter === 'skill') {
            value = Command.compileSkill(value)
          } else {
            value = () => undefined
          }
          break
        case 'state-getter':
          if (value?.getter === 'state') {
            value = Command.compileState(value)
          } else {
            value = () => undefined
          }
          break
        case 'equipment-getter':
          if (value?.getter === 'equipment') {
            value = Command.compileEquipment(value)
          } else {
            value = () => undefined
          }
          break
        case 'item-getter':
          if (value?.getter === 'item') {
            value = Command.compileItem(value)
          } else {
            value = () => undefined
          }
          break
        case 'element-getter':
          if (value?.getter === 'element') {
            value = Command.compileElement(value)
          } else {
            value = () => undefined
          }
          break
        case 'position-getter':
          if (value?.getter === 'position') {
            const getPoint = Command.compilePosition(value)
            value = () => {
              const point = getPoint()
              return point ? {x: point.x, y: point.y} : undefined
            }
          } else {
            value = () => undefined
          }
          break
        default:
          if (typeof value !== 'string') {
            value = defParameter.value
          }
          break
      }
      const pi = i * 2
      parameterList[pi] = key
      parameterList[pi + 1] = value
    }
    return parameterList
  }

  // 事件类型映射表(事件类型->脚本方法名称)
  static eventTypeMap = {
    update: 'update',
    create: 'onCreate',
    autorun: 'onStart',
    collision: 'onCollision',
    hittrigger: 'onHitTrigger',
    hitactor: 'onHitActor',
    destroy: 'onDestroy',
    playerenter: 'onPlayerEnter',
    playerleave: 'onPlayerLeave',
    actorenter: 'onActorEnter',
    actorleave: 'onActorLeave',
    skillcast: 'onSkillCast',
    skilladd: 'onSkillAdd',
    skillremove: 'onSkillRemove',
    stateadd: 'onStateAdd',
    stateremove: 'onStateRemove',
    equipmentadd: 'onEquipmentAdd',
    equipmentremove: 'onEquipmentRemove',
    itemuse: 'onItemUse',
    keydown: 'onKeyDown',
    keyup: 'onKeyUp',
    mousedown: 'onMouseDown',
    mousedownLB: 'onMouseDownLB',
    mousedownRB: 'onMouseDownRB',
    mouseup: 'onMouseUp',
    mouseupLB: 'onMouseUpLB',
    mouseupRB: 'onMouseUpRB',
    mousemove: 'onMouseMove',
    mouseenter: 'onMouseEnter',
    mouseleave: 'onMouseLeave',
    click: 'onClick',
    doubleclick: 'onDoubleClick',
    wheel: 'onWheel',
    input: 'onInput',
    focus: 'onFocus',
    blur: 'onBlur',
    destroy: 'onDestroy',
  }
}