parseDialogNode (dialogNode, shot, speakerToThangTypeSlugMap) {
    const soundCommands = []
    let speakerThangType = speakerToThangTypeSlugMap[dialogNode.speaker]
    if (dialogNode.textLocation && dialogNode.textLocation.y > 500) {
      // It's always mouse if it's low on the screen
      speakerThangType = 'mouse'
    }
    if (!speakerThangType) {
      // It's usually (always?) the hero
      speakerThangType = 'hero'
    }
    speakerThangType = standardizeSpeaker(speakerThangType)
    const soundEffects = getSoundEffects(dialogNode) || []
    soundEffects.forEach(({ sound, triggerStart }) => {
      const soundIdPromise = store.dispatch('audio/playSound', {
        track: 'soundEffects',
        autoplay: false,
        src: Object.values(sound).map(f => `/file/${f}`)
      })

      soundCommands.push(new SequentialCommands([
        new Sleep(triggerStart),
        new SyncFunction(async () => {
          const soundId = await soundIdPromise
          store.dispatch('audio/playSound', soundId)
        })
      ]))
    })

    const voiceOver = getVoiceOver(dialogNode)
    const hasText = dialogNode?.text && /[a-z]/i.test(dialogNode.text)
    if (voiceOver || hasText) {
      soundCommands.push(
        createVoiceOverCommand(dialogNode, speakerThangType)
      )
    } else {
      // If no voice over, cut any VO that may be playing.
      soundCommands.push(
        new SyncFunction(() => {
          store.dispatch('audio/fadeTrack', { to: 0, track: 'voiceOver', duration: 100 })
        })
      )
    }

    return soundCommands
  }
System interface method that CinematicParser calls on each dialogNode
of the cinematic.
@param {DialogNode} dialogNode
@param {Object} shot
@param {Object} speakerToThangTypeSlugMap
@returns {AbstractCommand[]}