trigger.js

'use strict'

// ******************************** 触发器类 ********************************

class Trigger {
  /** 触发器文件ID
   *  @type {string}
   */ id

  /** 触发器文件数据
   *  @type {Object}
   */ data

  /** 触发器水平位置
   *  @type {number}
   */ x

  /** 触发器垂直位置
   *  @type {number}
   */ y

  /** 触发器上一次水平位置
   *  @type {number}
   */ lastX

  /** 触发器上一次垂直位置
   *  @type {number}
   */ lastY

  /** 触发器缩放系数
   *  @type {number}
   */ scale

  /** 触发器角度(弧度)
   *  @type {number}
   */ angle

  /** 触发器移动速度(图块/秒)
   *  @type {number}
   */ speed

  /** 触发器水平速度分量
   *  @type {number}
   */ velocityX

  /** 触发器垂直速度分量
   *  @type {number}
   */ velocityY

  /** 当前帧的增量时间(毫秒)
   *  @type {number}
   */ deltaTime

  /** 触发器的总体播放速度
   *  @type {number}
   */ timeScale

  /** 触发器已经播放的时间
   *  @type {number}
   */ elapsed

  /** 触发器的持续时间
   *  @type {number}
   */ duration

  /** 触发器的形状参数对象
   *  @type {Object}
   */ shape

  /** 触发器的动画播放器
   *  @type {Animation}
   */ animation

  /** 触发器的角色选择器规则
   *  @type {string}
   */ selector

  /** 触发次数
   *  @type {number}
   */ hitCount

  /** 触发间隔(毫秒)
   *  @type {number}
   */ hitInterval

  /** 用于启用触发器的初始延时
   *  @type {number}
   */ initialDelay

  /** 用于禁用触发器的超时时间
   *  @type {number}
   */ timeout

  /** 触发器的更新器模块列表
   *  @type {ModuleList}
   */ updaters

  /** 触发器的事件映射表
   *  @type {Object}
   */ events

  /** 触发器的脚本管理器
   *  @type {Script}
   */ script

  /** 触发器的技能施放角色
   *  @type {Actor|null}
   */ caster

  /** 触发器正在施放的技能
   *  @type {Skill|null}
   */ skill

  /** 触发器击中的角色列表
   *  @type {Array<Actor>}
   */ hitList

  /** 触发器击中角色时的时间列表
   *  @type {Array<number>}
   */ timeList

   /** 检测触发器与墙块碰撞
   *  @type {Function}
   */ detectCollisionWithWalls

  /** 通过碰撞获取角色列表
   *  @type {Function}
   */ getActorsByCollision

  /** 通过触发模式获取角色列表
   *  @type {Function}
   */ getActorsByHitMode

  /** 更新时间列表
   *  @type {Function}
   */ updateTimeList

  /** 触发器的父级对象
   *  @type {SceneTriggerList|null}
   */ parent

  /** 已开始状态
   *  @type {boolean}
   */ started

  /**
   * 触发器对象
   * @param {TriggerFile} data 触发器文件数据
   */
  constructor(data) {
    this.id = data.id
    this.data = data
    this.x = 0
    this.y = 0
    this.lastX = 0
    this.lastY = 0
    this.scale = 1
    this.angle = 0
    this.speed = data.speed
    this.velocityX = 0
    this.velocityY = 0
    this.timeScale = 1
    this.deltaTime = 0
    this.elapsed = 0
    this.duration = data.duration
    this.shape = data.shape
    this.animation = null
    this.selector = data.selector
    this.detectCollisionWithWalls = Trigger.detectCollisionWithWalls[data.onHitWalls]
    this.getActorsByCollision = Trigger.actorGetters[data.shape.type]
    this.getActorsByHitMode = Trigger.collisionFilters[data.hitMode]
    this.updateTimeList = Trigger.hitListUpdaters[data.hitMode]
    switch (data.onHitActors) {
      case 'penetrate':
        this.hitCount = Infinity
        break
      case 'destroy':
        this.hitCount = 1
        break
      case 'penetrate-destroy':
        this.hitCount = data.hitCount
        break
    }
    this.hitInterval = data.hitInterval
    this.initialDelay = data.initialDelay
    this.timeout = data.initialDelay + (data.effectiveTime || Infinity)
    this.hitList = []
    this.timeList = []
    this.updaters = new ModuleList()
    this.events = data.events
    this.script = Script.create(this, data.scripts)
    this.caster = null
    this.skill = null
    this.parent = null
    this.started = false
    this.loadAnimation(data)
    Trigger.latest = this
  }

  /**
   * 加载触发器动画
   * @param {TriggerFile} data 触发器文件数据
   */
  loadAnimation(data) {
    const animData = Data.animations[data.animationId]
    if (animData !== undefined) {
      const animation = new Animation(animData)
      animation.parent = this
      animation.scale = this.scale
      animation.setPosition(this)
      animation.priority = data.priority
      animation.offsetY = data.offsetY
      animation.setMotion(data.motion)
      animation.redirect = animation.dirMap?.length > 1
      animation.rotatable = data.rotatable
      this.animation = animation
      if (this.duration === 0) {
        // 如果触发器持续时间是0,将会使用动画的持续时间
        this.duration = animation.length * Animation.step
      }
    }
  }

  /**
   * 更新触发器的运动和碰撞检测
   * @param {number} deltaTime 增量时间(毫秒)
   */
  update(deltaTime) {
    // 如果触发器过期,移除它
    if (this.elapsed >= this.duration) {
      this.remove()
      return
    }
    const time = deltaTime * this.timeScale
    // 计算增量时间(以秒为单位)
    this.deltaTime = time
    this.elapsed += time
    this.updaters.update(deltaTime)
    this.updateMovement()
    if (this.updateCollision()) {
      // 如果未与墙壁发生碰撞,更新动画
      this.updateAnimation(time)
    } else {
      // 否则移除
      this.remove()
    }
    // 更新上一次的位置
    this.lastX = this.x
    this.lastY = this.y
  }

  /**
   * 设置触发器位置
   * @param {number} x 水平位置
   * @param {number} y 垂直位置
   */
  setPosition(x, y) {
    this.x = x
    this.y = y
    this.lastX = x
    this.lastY = y
  }

  /**
   * 设置触发器缩放系数
   * @param {number} scale 触发器缩放系数
   */
  setScale(scale) {
    this.scale = scale
    if (this.animation) {
      this.animation.scale = scale
    }
  }

  /**
   * 设置触发器角度
   * @param {number} angle 触发器角度(弧度)
   */
  setAngle(angle) {
    this.angle = angle
    this.updateVelocity()
  }

  /**
   * 设置触发器速度
   * @param {number} speed 触发器速度(图块/秒)
   */
  setSpeed(speed) {
    this.speed = speed
    this.updateVelocity()
  }

  /** 更新触发器速度分量 */
  updateVelocity() {
    const cos = Math.cos(this.angle)
    const sin = Math.sin(this.angle)
    this.velocityX = this.speed * cos
    this.velocityY = this.speed * sin
  }

  /** 更新触发器的移动 */
  updateMovement() {
    const deltaTime = this.deltaTime / 1000
    this.x += this.velocityX * deltaTime
    this.y += this.velocityY * deltaTime
  }

  /** 更新触发器碰撞检测 */
  updateCollision() {
    // 检测与墙壁的碰撞,如果发生碰撞返回false
    if (this.detectCollisionWithWalls()) {
      return false
    }

    // 如果过去时间小于初始延时,或超时,则不会触发角色碰撞,返回true
    if (this.elapsed < this.initialDelay || this.elapsed >= this.timeout) return true

    // 获取碰撞角色列表(共享列表,用count表示长度)
    const targets = this.getActorsByCollision(this.x, this.y, this.angle, this.scale, this.shape)
    if (targets.count > 0) {
      // 通过选择器进一步筛选目标角色
      Trigger.getActorsBySelector(this.caster, this.selector)

      // 更新时间列表
      this.updateTimeList()

      // 获取命中的角色
      this.getActorsByHitMode()

      // 触发对应事件
      if (targets.count > 0) {
        const cmd1 = this.events.hitactor
        const {caster, skill} = this
        const {count} = targets
        for (let i = 0; i < count; i++) {
          const actor = targets[i]
          // 更新角色受击时间戳
          actor.updateHitTimestamp()
          const cmd2 = actor.events.hittrigger
          if (cmd2 !== undefined) {
            // 发送目标角色的击中触发器事件
            const event = new EventHandler(cmd2)
            event.triggerObject = this
            event.triggerSkill = skill
            event.triggerActor = actor
            event.casterActor = caster
            // 不需要对事件进行入栈和出栈
            // 不需要异步添加事件到更新器列表
            if (event.update() === false) {
              actor.updaters.add(event)
              event.onFinish(() => {
                Callback.push(() => {
                  actor.updaters.remove(event)
                })
              })
            }
          }
          // 同时发送脚本事件
          actor.script.emit('hittrigger', this)
          if (cmd1 !== undefined) {
            // 发送触发器的击中角色事件
            const event = new EventHandler(cmd1)
            event.triggerObject = this
            event.triggerSkill = skill
            event.triggerActor = actor
            event.casterActor = caster
            // 不需要对事件进行入栈和出栈
            // 不需要异步添加事件到更新器列表
            if (event.update() === false) {
              actor.updaters.add(event)
              event.onFinish(() => {
                Callback.push(() => {
                  actor.updaters.remove(event)
                })
              })
            }
          }
          // 同时发送脚本事件
          this.script.emit('hitactor', actor)
        }
        // 如果击中次数不够,返回false
        if ((this.hitCount -= count) <= 0) {
          return false
        }
      }
    } else {
      // 更新时间列表
      this.updateTimeList()
    }
    return true
  }

  /**
   * 更新触发器动画播放进度
   * @param {number} deltaTime 增量时间(毫秒)
   */
  updateAnimation(deltaTime) {
    const {animation} = this
    // 如果不存在动画,返回
    if (animation === null) return
    if (animation.redirect) {
      // 如果开启了动画方向计算
      this.calculateAnimDirection()
    } else if (animation.rotatable) {
      // 如果开启了动画旋转,调整旋转角度
      animation.rotation = this.angle
    }
    // 更新动画
    animation.update(deltaTime)
  }

  /** 计算触发器的动画方向 */
  calculateAnimDirection() {
    const {animation} = this
    // 设置默认动画方向为技能释放者的动画方向
    if (!animation.casterDirSync) {
      animation.casterDirSync = true
      const casterDir = this.caster.animation?.direction
      if (casterDir >= 0) {
        animation.setDirection(casterDir)
      }
    }
    // 设置触发器动画角度
    animation.setAngle(this.angle)
  }

  /** 移除触发器 */
  remove() {
    this.destroy()
    // 延迟从触发器列表中移除自己
    Callback.push(() => {
      Scene.triggers.remove(this)
    })
  }

  /**
   * 调用触发器事件
   * @param {string} type 触发器事件类型
   * @returns {EventHandler|undefined}
   */
  callEvent(type) {
    const commands = this.events[type]
    if (commands) {
      const event = new EventHandler(commands)
      event.triggerObject = this
      event.triggerSkill = this.skill
      event.triggerActor = this.caster
      event.casterActor = this.caster
      return EventHandler.call(event, this.updaters)
    }
  }

  /**
   * 调用触发器事件和脚本
   * @param {string} type 触发器事件类型
   */
  emit(type) {
    this.callEvent(type)
    this.script.emit(type, this)
  }

  // 自动执行
  autorun() {
    if (this.started === false) {
      this.started = true
      this.emit('autorun')
    }
  }

  /** 销毁触发器 */
  destroy() {
    this.emit('destroy')
    this.animation?.destroy()
  }

  // 最近创建触发器
  static latest

  // 临时角色列表
  static actors = []

  // 与角色形状发生碰撞
  static collideWithActorShape = false

  // 初始化
  static initialize() {
    this.collideWithActorShape = Data.config.collision.trigger.collideWithActorShape
  }

  /** 擦除角色缓存列表中的数据 */
  static update() {
    // 擦除角色缓存列表
    let i = 0
    const actors = this.actors
    while (actors[i] !== undefined) {
      actors[i++] = undefined
    }
  }

  /**
   * 获取指定选择器筛选的角色
   * 存放到角色缓存列表
   * @param {Actor} caster 技能施放角色
   * @param {Object} selector 选择器对象
   */
  static getActorsBySelector(caster, selector) {
    const actors = this.actors
    let count = 0
    if (caster) {
      const inspector = Actor.inspectors[selector]
      const length = actors.count
      for (let i = 0; i < length; i++) {
        const actor = actors[i]
        if (inspector(caster, actor)) {
          actors[count++] = actor
        }
      }
    }
    actors.count = count
  }

  // 触发器角色获取器
  static actorGetters = new class {
    /**
     * 获取矩形碰撞区域中的角色
     * @param {number} x 触发器位置X
     * @param {number} y 触发器位置Y
     * @param {number} angle 触发器角度(弧度)
     * @param {number} scale 触发器缩放系数
     * @param {Object} shape 触发器形状参数对象
     * @returns {Actor[]} 角色缓存列表
     */
    'rectangle' = (x, y, angle, scale, shape) => {
      let count = 0
      const targets = Trigger.actors
      const width = shape.width * scale
      const height = shape.height * scale
      const anchor = shape.anchor
      const cos = Math.cos(angle)
      const sin = Math.sin(angle)
      const left = -width * anchor
      const top = -height / 2
      const right = width + left
      const bottom = height / 2
      // 计算矩形触发区域的四个顶点位置
      const x1 = left * cos - top * sin
      const y1 = left * sin + top * cos
      const x2 = left * cos - bottom * sin
      const y2 = left * sin + bottom * cos
      const x3 = right * cos - top * sin
      const y3 = right * sin + top * cos
      const x4 = right * cos - bottom * sin
      const y4 = right * sin + bottom * cos
      const tl = x + Math.min(x1, x2, x3, x4)
      const tt = y + Math.min(y1, y2, y3, y4)
      const tr = x + Math.max(x1, x2, x3, x4)
      const tb = y + Math.max(y1, y2, y3, y4)
      const expansion = Trigger.collideWithActorShape ? Scene.binding.maxColliderHalf : 0
      // 获取矩形触发区域所在的角色分区列表
      const cells = Scene.actors.cells.get(
        tl - expansion,
        tt - expansion,
        tr + expansion,
        tb + expansion,
      )
      const length = cells.count
      for (let i = 0; i < length; i++) {
        const actors = cells[i]
        const length = actors.length
        for (let i = 0; i < length; i++) {
          const actor = actors[i]
          // 如果角色已激活
          if (actor.active) {
            if (Trigger.collideWithActorShape) {
              switch (actor.collider.shape) {
                case 'circle': {
                  // 计算角色的相对位置
                  const rx = actor.x - x
                  const ry = actor.y - y
                  // 以触发区域中心为锚点
                  // 逆旋转角色的相对位置
                  const ox = rx * cos + ry * sin
                  const oy = ry * cos - rx * sin
                  const closestX = Math.clamp(ox, left, right)
                  const closestY = Math.clamp(oy, top, bottom)
                  if ((ox - closestX) ** 2 + (oy - closestY) ** 2 < actor.collider.half ** 2) {
                    targets[count++] = actor
                  }
                  continue
                }
                case 'square': {
                  // 投影 - 1
                  const ah = actor.collider.half
                  if (actor.x - ah >= tr || actor.x + ah <= tl || actor.y - ah >= tb || actor.y + ah <= tt) {
                    continue
                  }
                  // 投影 - 2
                  const al = actor.x - x - ah
                  const at = actor.y - y - ah
                  const ar = actor.x - x + ah
                  const ab = actor.y - y + ah
                  const x1 = al * cos + at * sin
                  const y1 = at * cos - al * sin
                  const x2 = al * cos + ab * sin
                  const y2 = ab * cos - al * sin
                  const x3 = ar * cos + ab * sin
                  const y3 = ab * cos - ar * sin
                  const x4 = ar * cos + at * sin
                  const y4 = at * cos - ar * sin
                  const rl = Math.min(x1, x2, x3, x4)
                  const rt = Math.min(y1, y2, y3, y4)
                  const rr = Math.max(x1, x2, x3, x4)
                  const rb = Math.max(y1, y2, y3, y4)
                  if (rl >= right || rr <= left || rt >= bottom || rb <= top) {
                    continue
                  }
                  targets[count++] = actor
                  continue
                }
              }
            } else {
              // 计算角色的相对位置
              const rx = actor.x - x
              const ry = actor.y - y
              // 以触发区域中心为锚点
              // 逆旋转角色的相对位置
              const ox = rx * cos + ry * sin
              const oy = ry * cos - rx * sin
              // 如果角色的锚点位于矩形触发区域中,则添加到目标列表中
              if (ox >= left && ox < right && oy >= top && oy < bottom) {
                targets[count++] = actor
              }
            }
          }
        }
      }
      targets.count = count
      return targets
    }

    /**
     * 获取圆形碰撞区域中的角色
     * @param {number} x 触发器位置X
     * @param {number} y 触发器位置Y
     * @param {number} angle 触发器角度(弧度)
     * @param {Object} shape 触发器形状参数对象
     * @returns {Actor[]} 角色缓存列表
     */
    'circle' = (x, y, angle, scale, shape) => {
      let count = 0
      const targets = Trigger.actors
      const radius = shape.radius * scale
      const expansion = Trigger.collideWithActorShape ? Scene.binding.maxColliderHalf : 0
      // 获取圆形触发区域所在的角色分区列表
      const cells = Scene.actors.cells.get(
        x - radius - expansion,
        y - radius - expansion,
        x + radius + expansion,
        y + radius + expansion,
      )
      const length = cells.count
      for (let i = 0; i < length; i++) {
        const actors = cells[i]
        const length = actors.length
        for (let i = 0; i < length; i++) {
          const actor = actors[i]
          // 如果角色已激活
          if (actor.active) {
            if (Trigger.collideWithActorShape) {
              switch (actor.collider.shape) {
                case 'circle':
                  if ((x - actor.x) ** 2 + (y - actor.y) ** 2 < (radius + actor.collider.half) ** 2) {
                    targets[count++] = actor
                  }
                  continue
                case 'square':
                  const ox = x - actor.x
                  const oy = y - actor.y
                  const half = actor.collider.half
                  const closestX = Math.clamp(ox, -half, half)
                  const closestY = Math.clamp(oy, -half, half)
                  if ((ox - closestX) ** 2 + (oy - closestY) ** 2 < radius ** 2) {
                    targets[count++] = actor
                  }
                  continue
              }
            } else {
              // 如果角色的锚点位于圆形触发区域中,则添加到目标列表中
              if ((x - actor.x) ** 2 + (y - actor.y) ** 2 < radius ** 2) {
                targets[count++] = actor
              }
            }
          }
        }
      }
      targets.count = count
      return targets
    }

    /**
     * 获取扇形碰撞区域中的角色
     * @param {number} x 触发器位置X
     * @param {number} y 触发器位置Y
     * @param {number} angle 触发器角度(弧度)
     * @param {Object} shape 触发器形状参数对象
     * @returns {Actor[]} 角色缓存列表
     */
    'sector' = (x, y, angle, scale, shape) => {
      let count = 0
      const targets = Trigger.actors
      const radius = shape.radius * scale
      const cos = Math.cos(angle)
      const sin = Math.sin(angle)
      const expansion = Trigger.collideWithActorShape ? Scene.binding.maxColliderHalf : 0
      // 获取圆形触发区域所在的角色分区列表
      const cells = Scene.actors.cells.get(
        x - radius - expansion,
        y - radius - expansion,
        x + radius + expansion,
        y + radius + expansion,
      )
      const length = cells.count
      for (let i = 0; i < length; i++) {
        const actors = cells[i]
        const length = actors.length
        for (let i = 0; i < length; i++) {
          const actor = actors[i]
          // 如果角色已激活
          if (actor.active) {
            const rx = actor.x - x
            const ry = actor.y - y
            if (Trigger.collideWithActorShape) {
              switch (actor.collider.shape) {
                case 'circle': {
                  const square = rx ** 2 + ry ** 2
                  const half = actor.collider.half
                  if (square < (radius + half) ** 2) {
                    const centralAngle = Math.radians(shape.centralAngle)
                    const angle1 = angle - centralAngle / 2
                    const angle2 = angle + centralAngle / 2
                    const angle3 = centralAngle + Math.PI / 2
                    const angle4 = Math.PI * 1.5
                    const angle5 = Math.PI + centralAngle / 2
                    const relativeAngle = Math.modRadians(Math.atan2(ry, rx) - angle1)
                    // 如果角色位于扇形区域内,则添加到目标列表中
                    if (relativeAngle <= centralAngle) {
                      targets[count++] = actor
                    } else if (
                      relativeAngle >= angle3 &&
                      relativeAngle <= angle4) {
                      if (square < half ** 2) {
                        targets[count++] = actor
                      }
                    } else {
                      const angle = relativeAngle > angle5 ? angle1 : angle2
                      const cos = Math.cos(angle)
                      const sin = Math.sin(angle)
                      const ox = rx * cos + ry * sin
                      const oy = ry * cos - rx * sin
                      const px = ox < radius ? ox : radius
                      if ((px - ox) ** 2 + oy ** 2 < half ** 2) {
                        targets[count++] = actor
                      }
                    }
                  }
                  continue
                }
                case 'square': {
                  const ox = x - actor.x
                  const oy = y - actor.y
                  const half = actor.collider.half
                  const closestX = Math.clamp(ox, -half, half)
                  const closestY = Math.clamp(oy, -half, half)
                  if ((ox - closestX) ** 2 + (oy - closestY) ** 2 < radius ** 2) {
                    // 以触发区域中心为锚点
                    // 逆旋转角色的相对位置
                    const ox = rx * cos + ry * sin
                    const oy = ry * cos - rx * sin
                    const angle0 = Math.atan2(oy, ox)
                    const centralAngle = Math.radians(shape.centralAngle)
                    const halfAngle = centralAngle / 2
                    // 如果角色位于扇形区域内,则添加到目标列表中
                    if (angle0 > -halfAngle && angle0 < halfAngle) {
                      targets[count++] = actor
                    } else {
                      const ox = actor.x - x
                      const oy = actor.y - y
                      const ol = ox - half
                      const ot = oy - half
                      const or = ox + half
                      const ob = oy + half
                      const angle1 = Math.modRadians(Math.atan2(ot, ol) - angle + halfAngle)
                      const angle2 = Math.modRadians(Math.atan2(ob, ol) - angle + halfAngle)
                      const angle3 = Math.modRadians(Math.atan2(ob, or) - angle + halfAngle)
                      const angle4 = Math.modRadians(Math.atan2(ot, or) - angle + halfAngle)
                      if (Math.min(angle1, angle2, angle3, angle4) < centralAngle) {
                        targets[count++] = actor
                      }
                    }
                  }
                  continue
                }
              }
            } else {
              // 如果角色的锚点位于圆形触发区域中
              if (rx ** 2 + ry ** 2 < radius ** 2) {
                // 以触发区域中心为锚点
                // 逆旋转角色的相对位置
                const ox = rx * cos + ry * sin
                const oy = ry * cos - rx * sin
                const angle = Math.degrees(Math.atan2(oy, ox))
                const halfAngle = shape.centralAngle / 2
                // 如果角色位于扇形区域内,则添加到目标列表中
                if (angle > -halfAngle && angle < halfAngle) {
                  targets[count++] = actor
                }
              }
            }
          }
        }
      }
      targets.count = count
      return targets
    }
  }

  // 检测与墙壁的碰撞
  static detectCollisionWithWalls = new class {
    /**
     * 检测与墙壁的碰撞 - 穿透
     * @returns {false}
     */
    'penetrate' = () => false

    /**
     * 检测与墙壁的碰撞 - 销毁
     * @returns {boolean} 是否发生了碰撞
     */
    'destroy' = function () {
      return !this.parent.scene.isInLineOfSight(this.lastX, this.lastY, this.x, this.y)
    }
  }

  // 碰撞过滤器
  static collisionFilters = new class {
    /**
     * 碰撞过滤器 - 一次
     * @returns {Actor[]} 角色缓存列表
     */
    'once' = function () {
      let count = 0
      const actors = Trigger.actors
      const hitList = this.hitList
      const length = actors.count
      for (let i = 0; i < length; i++) {
        const actor = actors[i]
        // 如果角色未在碰撞列表中,添加它
        if (!hitList.includes(actor)) {
          actors[count++] = actor
          hitList.push(actor)
        }
      }
      actors.count = count
      return actors
    }

    /** 碰撞过滤器 - 碰撞期间一次 */
    'once-on-overlap' = this.once

    /**
     * 碰撞过滤器 - 重复
     * @returns {Actor[]} 角色缓存列表
     */
    'repeat' = function () {
      let count = 0
      const actors = Trigger.actors
      const time = this.elapsed
      const hitInterval = this.hitInterval
      const hitList = this.hitList
      const timeList = this.timeList
      const length = actors.count
      for (let i = 0; i < length; i++) {
        const actor = actors[i]
        const index = hitList.indexOf(actor)
        // 如果角色未在碰撞列表中,添加它
        if (index === -1) {
          actors[count++] = actor
          hitList.push(actor)
          timeList.push(time)
          continue
        }
        // 如果已经过了碰撞间隔,可以再次碰撞
        const elapsed = time - timeList[index]
        if (elapsed >= hitInterval) {
          timeList[index] += hitInterval
          actors[count++] = actor
          continue
        }
      }
      actors.count = count
      return actors
    }
  }

  // 击中列表更新器
  static hitListUpdaters = new class {
    /** 击中列表更新器 - 一次 */
    'once' = Function.empty

    /** 击中列表更新器 - 碰撞期间一次 */
    'once-on-overlap' = function () {
      if (this.hitList.length > 0) {
        const actors = Trigger.actors
        const count = actors.count
        const hitList = this.hitList
        let i = hitList.length
        outer: while (--i >= 0) {
          const actor = hitList[i]
          // 如果已碰撞的角色还在本轮目标列表中,继续
          for (let i = 0; i < count; i++) {
            if (actors[i] === actor) {
              continue outer
            }
          }
          // 已碰撞的角色已经脱离触发器,移除
          hitList.splice(i, 1)
        }
      }
    }

    /** 击中列表更新器 - 重复 */
    'repeat' = function () {
      if (this.hitList.length > 0) {
        const actors = Trigger.actors
        const count = actors.count
        const time = this.elapsed
        const hitInterval = this.hitInterval
        const hitList = this.hitList
        const timeList = this.timeList
        let i = hitList.length
        outer: while (--i >= 0) {
          const actor = hitList[i]
          // 如果已碰撞的角色还在本轮目标列表中,继续
          for (let i = 0; i < count; i++) {
            if (actors[i] === actor) {
              continue outer
            }
          }
          // 已碰撞的角色已经脱离触发器
          // 且已经过了碰撞间隔,移除
          if (time - timeList[i] >= hitInterval) {
            hitList.splice(i, 1)
            timeList.splice(i, 1)
          }
        }
      }
    }
  }
}