local class = require("core.class")
local thunk = require("core.thunk")
local runlistlib = require("core.runlist")
local g_runList = runlistlib.RunList.New()
local g_soundEmitter
local find_symbol_name = function(sym)
  for k, v in pairs(_G) do
    if v == sym then
      return k
    end
  end
  local subobj_root = engine.DebugGetSubObjectEnvironmentRoot and engine.DebugGetSubObjectEnvironmentRoot()
  if subobj_root then
    for _, env in pairs(subobj_root) do
      for k, v in pairs(env) do
        if v == sym then
          return k
        end
      end
    end
  end
  local pm = _G.__profilemarkers[sym]
  if pm then
    return pm
  end
  return "<unknown>"
end
local draw_debug_sequence_points
function draw_debug_sequence_points(debugTable, sequence, current_index, depth)
  for i, sequence_point in ipairs(sequence) do
    local done = i < current_index
    local done_str = done and "[done] " or ""
    local name, desc = sequence_point.debug_func()
    debugTable[#debugTable + 1] = {
      string.rep(" ", depth * 2) .. (name or "<unknown>"),
      done_str .. (desc or "")
    }
    if sequence_point.multiple_wait_list then
      draw_debug_sequence_points(debugTable, sequence_point.multiple_wait_list, done and 1000 or 1, depth + 1)
    end
  end
end
local g_vfsDebug
thunk.Install("OnScriptLoaded", function(lvl)
  g_vfsDebug = engine.VFSBool.New("Show CineSequence Debug")
end)
local draw_debug_sequence_table = function(level)
  local debugTable = {}
  local sorted_sequences = {}
  for seq in pairs(g_runList.references) do
    sorted_sequences[#sorted_sequences + 1] = seq
  end
  table.sort(sorted_sequences, function(a, b)
    return (a.elapsed_time or 0) < (b.elapsed_time or 0)
  end)
  for _, seq in ipairs(sorted_sequences) do
    debugTable[#debugTable + 1] = {
      seq.name,
      seq.elapsed_time
    }
    draw_debug_sequence_points(debugTable, seq.sequence, seq.current_index or 1, 1)
  end
  local r, g, b = 0, 128, 0
  local color = engine.Vector.New(r, g, b)
  debugTable.Title = string.format("Running CineSequences (%s)", level.Name)
  debugTable.TitleColor = color
  debugTable.X, debugTable.Y = 2, 2
  engine.DrawDebugTable(debugTable)
end
thunk.Install("OnUpdate", function(lvl)
  g_runList:Update(lvl:GetUnitTime())
  if engine.IsDebug() and g_vfsDebug ~= nil and g_vfsDebug.value == true then
    draw_debug_sequence_table(lvl)
  end
end)
local CineSequence = class.Class("CineSequence")
function CineSequence:init(level, level_obj, name)
  if not level then
    return error("Expected Level as first argument to CineSequence")
  end
  if not level_obj then
    return error("Expected GameObject as second argument to CineSequence")
  end
  if not name then
    return error("Expected a name string as third argument to CineSequence")
  end
  self.level = level
  self.level_obj = level_obj
  self.name = name
  self.skipping = false
  self.ableToSkip = false
  self.clearMusicOnSkip = true
  self.skipTimer = 0
  self.musicToSetPostSkip = {}
  self.queuedSkipEnd = false
  self.sequence = {}
  self.triggered = false
  g_runList:add(self)
end
local run_sequence = function(sequence, start_index, elapsed_time, skipping)
  local current_index = start_index
  local should_advance = true
  local finished = false
  while should_advance do
    local sequence_point = sequence[current_index]
    if not sequence_point then
      finished = true
      break
    end
    local time_absolute = sequence_point.time_absolute
    local condition_func = sequence_point.condition_func
    should_advance = not time_absolute or elapsed_time >= time_absolute
    if should_advance and condition_func then
      should_advance = condition_func()
    end
    if should_advance then
      local callback_func = sequence_point.callback_func
      local args = sequence_point.args
      if callback_func and args then
        callback_func(unpack(args))
      elseif callback_func then
        callback_func()
      end
      current_index = current_index + 1
    end
  end
  return finished, current_index
end
function CineSequence:Update(dt)
  if not self.triggered then
    if self.branch_from_sequence then
      return
    else
      return error("Trigger not called on CineSequence")
    end
  end
  if self.done then
    return
  end
  self.current_dt = dt
  local current_index = self.current_index or 1
  local elapsed_time = (self.elapsed_time or 0) + dt
  local done, new_index = run_sequence(self.sequence, current_index, elapsed_time, self.skipping)
  if done then
    self:Complete()
    return
  end
  self.current_index = new_index
  self.elapsed_time = elapsed_time
  if self.skipping == true then
    self.skipTimer = game.UI.GetFrameTime() + self.skipTimer
    if self.skipTimer > 0.8 then
      if self.queuedSkipEnd then
        self:EndSkipIfActive()
        self.queuedSkipEnd = false
        return
      end
      game.Audio.InterruptAllBanter()
      if game.Player.FindPlayer():IsDoingSyncMove() then
        local myTime = game.Player.FindPlayer():GetAnimTimeTotalTime()
        engine.DrawText2D("CAMERA TILT UP: " .. myTime, 100, 100, 16777215, 0, 1.05)
        local speedUp = myTime / 3
        if speedUp < 20 then
          speedUp = 20
        end
        if self.maxSpeedup and speedUp > self.maxSpeedup then
          speedUp = self.maxSpeedup
        end
        game.SetTimeScale(speedUp)
      else
        local speedUp = 5
        if self.maxSpeedup and speedUp > self.maxSpeedup then
          speedUp = self.maxSpeedup
        end
        game.SetTimeScale(speedUp)
      end
    end
  end
end
function CineSequence:MarkAbleToSkip(optionalArgs)
  if game.UI.CineSkipLoadScreenOn and game.SetAbleToSkip then
    game.SetAbleToSkip(true)
    self.ableToSkip = true
    if optionalArgs then
      if optionalArgs.alreadySkipping then
        self:OnSequenceSkipped()
      end
      if optionalArgs.callbackOnSkip then
        self.callbackOnSkip = optionalArgs.callbackOnSkip
      end
    end
  end
end
function CineSequence:SetMusicClearingOnSkip(clearMusic)
  local set_clearMusic = function()
    self.clearMusicOnSkip = clearMusic
  end
  self:AddSequencePoint({
    callback_func = set_clearMusic,
    args = {self},
    debug_func = function()
      return "Do", find_symbol_name(set_clearMusic)
    end
  })
end
function CineSequence:MarkManualEndSkip(manualEndSkip)
  self.manualEndSkip = manualEndSkip
end
local disableControllerInput = function()
  if game.CHECK_FEATURE("CINE_SKIP_INPUT_LOCK") then
    local pad = game.Player.FindPlayer().Pad
    pad:UIDisableHack(true, 2)
  end
end
local enableControllerInput = function()
  if game.CHECK_FEATURE("CINE_SKIP_INPUT_LOCK") then
    local pad = game.Player.FindPlayer().Pad
    pad:UIDisableHack(false, 2)
  end
end
function CineSequence:OnSequenceSkipped()
  if self.triggered and self.ableToSkip then
    self.skipping = true
    game.Camera.CancelRecenter()
    if self.clearMusicOnSkip then
      game.Audio.StartMusic("SND_MX_STOP_ALL_MUSIC")
    end
    if g_soundEmitter == nil then
      g_soundEmitter = game.Player.FindPlayer():FindSingleSoundEmitterByName("SNDKratos")
    end
    if g_soundEmitter ~= nil then
      g_soundEmitter:Start("SND_Stop_All_CINE_Stems")
    end
    game.Audio.MusicFadeOutAndLogVolume(1)
    disableControllerInput()
    self.skipTimer = 0
    game.UI.CineSkipLoadScreenOn()
    if game.Cinematics.AlertCinematicSkipped then
      game.Cinematics.AlertCinematicSkipped(self.name)
    end
    engine.SendHook("UI_CALL_EVENT", engine.GetUIWad(), "EVT_Journal_Log_Pause")
    if self.callbackOnSkip then
      self.callbackOnSkip()
    end
  end
end
function _G.OnSequenceSkipped()
  for seq in pairs(g_runList.references) do
    seq:OnSequenceSkipped()
  end
end
function CineSequence:SetPostSkipMusic(master, master2, master3, master4, normalMusic, checkpointMusic, master5)
  local set_music = function()
    self.musicToSetPostSkip.master = master
    self.musicToSetPostSkip.master2 = master2
    self.musicToSetPostSkip.master3 = master3
    self.musicToSetPostSkip.master4 = master4
    self.musicToSetPostSkip.master5 = master5
    self.musicToSetPostSkip.normalMusic = normalMusic
    self.musicToSetPostSkip.checkpointMusic = checkpointMusic
  end
  self:AddSequencePoint({
    callback_func = set_music,
    args = {self},
    debug_func = function()
      return "Do", find_symbol_name(set_music)
    end
  })
end
function CineSequence:SetMaxSkipSpeedup(maxSpeedup)
  self.maxSpeedup = maxSpeedup
end
local RestoreSound = function(self)
  if g_soundEmitter ~= nil then
    g_soundEmitter:Start("SND_Stop_All_CINE_Stems_No_Delay")
  end
  if self.musicToSetPostSkip.master ~= nil then
    game.Audio.StartMusicMaster(self.musicToSetPostSkip.master)
  end
  if self.musicToSetPostSkip.master2 ~= nil then
    game.Audio.StartMusicMaster(self.musicToSetPostSkip.master2)
  end
  if self.musicToSetPostSkip.master3 ~= nil then
    game.Audio.StartMusicMaster(self.musicToSetPostSkip.master3)
  end
  if self.musicToSetPostSkip.master4 ~= nil then
    game.Audio.StartMusicMaster(self.musicToSetPostSkip.master4)
  end
  if self.musicToSetPostSkip.master5 ~= nil then
    game.Audio.StartMusicMaster(self.musicToSetPostSkip.master5)
  end
  if self.musicToSetPostSkip.normalMusic ~= nil then
    game.Audio.StartMusic(self.musicToSetPostSkip.normalMusic)
  end
  if self.musicToSetPostSkip.checkpointMusic ~= nil then
    game.Audio.StartCheckpointedMusic(self.musicToSetPostSkip.checkpointMusic)
  end
end
function CineSequence:EndSkipIfActive(sequenceEnding, dontFadeInMusic)
  if self.skipping then
    if self.skipTimer < 0.8 and not sequenceEnding then
      self.queuedSkipEnd = true
      return
    end
    if game.ResetClothAllCreatures then
      game.ResetClothAllCreatures()
    end
    game.SetTimeScale(1)
    self.skipTimer = 0
    game.UI.CineSkipLoadScreenOff()
    engine.SendHook("UI_CALL_EVENT", engine.GetUIWad(), "EVT_Journal_Log_Unpause")
    enableControllerInput()
    RestoreSound(self)
    if dontFadeInMusic == nil or dontFadeInMusic == false then
      game.Audio.MusicFadeIn(1)
    end
    self.skipping = false
  end
  if self.ableToSkip and game.SetAbleToSkip then
    game.SetAbleToSkip(false)
  end
  self.ableToSkip = false
end
function CineSequence:Complete()
  self.done = true
  if not self.manualEndSkip then
    self:EndSkipIfActive(true)
  end
  g_runList:remove(self)
end
function CineSequence:AddSequencePoint(sequence_point)
  if self.triggered then
    return error("Can't add a sequence point to a running CineSequence")
  end
  if self.multiple_wait_list then
    return error("Can't add an action to a CineSequence between BeginMultipleWait and EndMultipleWait")
  end
  self.sequence[#self.sequence + 1] = sequence_point
end
function CineSequence:AddWaitSequencePoint(sequence_point)
  if self.triggered then
    return error("Can't add a sequence point to a running CineSequence")
  end
  if self.multiple_wait_list then
    self.multiple_wait_list[#self.multiple_wait_list + 1] = sequence_point
  else
    self.sequence[#self.sequence + 1] = sequence_point
  end
end
local internal_start_sequence = function(self)
  g_runList:add(self)
  self.triggered = true
  local dt = self.level:GetUnitTime()
  self:Update(dt)
end
function CineSequence:StartSequence()
  if self.triggered then
    return error("Can't call StartSequence on a CineSequence that's already running")
  end
  if self.multiple_wait_list then
    return error("Call to StartSequence without call to EndMultipleWait")
  end
  if self.branch_from_sequence then
    return error("Call to StartSequence on a CineSequence that's referenced by BranchSequence")
  end
  if game.Cinematics.SetLastSequenceBeginTime then
    game.Cinematics.SetLastSequenceBeginTime()
  end
  internal_start_sequence(self)
end
function CineSequence:Do(callback_func)
  self:AddSequencePoint({
    callback_func = callback_func,
    args = {self},
    debug_func = function()
      return "Do", find_symbol_name(callback_func)
    end
  })
end
function CineSequence:WaitForSequence(seq)
  local local_done = false
  local seq_name = seq.name
  local condition_func = function()
    if not local_done and seq.done then
      local_done = true
      seq = nil
    end
    return local_done
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForSequence", seq_name
    end
  })
end
function CineSequence:BranchSequence(seq)
  if seq.branch_from_sequence ~= nil then
    return error("Sequence '", seq.name, "' already branches from sequence '", seq.branch_from_sequence.name, "'")
  end
  g_runList:remove(seq)
  seq.branch_from_sequence = self
  local branch_sequence = function()
    internal_start_sequence(seq)
  end
  self:AddSequencePoint({
    callback_func = branch_sequence,
    args = {self},
    debug_func = function()
      return "BranchSequence", seq.name
    end
  })
end
function CineSequence:CancelSequence(seq)
  local cancel_sequence = function()
    seq:Complete()
  end
  self:AddSequencePoint({
    callback_func = cancel_sequence,
    args = {self},
    debug_func = function()
      return "CancelSequence", seq.name
    end
  })
end
function CineSequence:SpawnActorAndWait(actor)
  self:AddSequencePoint({
    callback_func = actor.Spawn,
    args = {actor},
    debug_func = function()
      return "SpawnActor", actor.name
    end
  })
  local frameCount = 0
  local condition_func = function()
    frameCount = frameCount + 1
    return 1 < frameCount and actor:CatchCreature() ~= nil
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "SpawnActor (wait)", actor.name
    end
  })
end
function CineSequence:WaitForActorInZone(actor, zone_name)
  local condition_func = function()
    local creature = actor:GetCreature()
    return creature:InsideZone(zone_name) or false
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForActorInZone", actor.name .. " (" .. tostring(zone_name) .. ")"
    end
  })
end
function CineSequence:WaitForActorOutsideZone(actor, zone_name)
  local condition_func = function()
    local creature = actor:GetCreature()
    return not creature:InsideZone(zone_name) and true
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForActorOutsideZone", actor.name .. " (" .. tostring(zone_name) .. ")"
    end
  })
end
function CineSequence:ActorForceApproachAndWait(actor, arg_table)
  local pos = arg_table.pos
  local dir = arg_table.dir
  local branch_name = arg_table.Branch
  local joint_name = arg_table.ReferenceJoint
  local speed = arg_table.speed
  local stop = arg_table.stop
  local foot = arg_table.foot
  local weapon_state = arg_table.weapon_state
  local completionPercentage = arg_table.completion_percentage
  local ignore_navmesh = arg_table.ignore_navmesh
  local strafe_distance = arg_table.strafe_distance or 2
  local prevent_path_eval = arg_table.prevent_path_eval
  local radius = arg_table.radius or 1.5
  local focus_end_direction = false
  local weaponType = ""
  local useCompletionPercentage = false
  local finalEquipMove = arg_table.INT8_HACK_final_equip_move
  if branch_name and joint_name then
    focus_end_direction = true
  end
  if completionPercentage then
    useCompletionPercentage = true
  end
  local stop_distance
  if stop then
    stop_distance = radius
  elseif stop == nil then
    engine.Warning("Warning: Cine's \"stop\" argument is nil. This should not be the case.")
  end
  local complete_radius = radius
  if arg_table.stop_distance ~= nil then
    engine.Warning("Warning: CineSequence no longer usings stop_distance, just use \"radius\"")
  end
  if arg_table.complete_radius ~= nil then
    engine.Warning("Warning: CineSequence no longer usings complete_radius, just use \"radius\"")
  end
  if (not branch_name or not joint_name) and not pos then
    return error("Must specify Branch and ReferenceJoint or pos for ActorForceApproachAndWait")
  end
  local done = false
  local puppeteer
  local HasArrived = function()
    actor:GetCreature():ClearFocus()
    done = true
  end
  local set_weapon_state = function()
    if weapon_state then
      if actor:GetCreature() == game.Player.FindPlayer() then
        if weapon_state == "sheathed" or weapon_state == "bare" then
          weaponType = "Bare"
        elseif weapon_state == "axe" then
          weaponType = "Axe"
        elseif weapon_state == "blades" then
          weaponType = "Blades"
        elseif weapon_state == "bare_on_back" then
          weaponType = "BareOnBack"
        else
          engine.Warning("Warning: attempting to alter player's weapon state with unknown string value. Check script for typos.")
        end
        puppeteer:WeaponEquip({
          weaponMode = weaponType,
          use_completion_percentage = useCompletionPercentage,
          completion_percentage = completionPercentage,
          final_move = finalEquipMove
        })
      else
        engine.Warning("Warning: attempting to alter weapon state on creature other than kratos. This is not currently supported by sequencer")
      end
    end
  end
  local start_approach = function()
    puppeteer = actor:StartPuppetingForce(self.level_obj)
    local approach_arrive_params = {
      pos = pos,
      dir = dir,
      branch_name = branch_name,
      joint_name = joint_name,
      speed = speed,
      stop = stop,
      ignore_navmesh = ignore_navmesh,
      strafe_distance = strafe_distance,
      radius = radius,
      complete_radius = complete_radius,
      stop_distance = stop_distance,
      foot = foot,
      focus_end_direction = focus_end_direction
    }
    if not prevent_path_eval then
      puppeteer:SetType("KeepEvaluatingPath")
    end
    puppeteer:Approach(approach_arrive_params)
    set_weapon_state()
    if stop then
      puppeteer:OnComplete(HasArrived)
    else
      puppeteer:OnArrival(HasArrived, approach_arrive_params)
    end
  end
  self:AddSequencePoint({
    callback_func = start_approach,
    debug_func = function()
      return "ActorForceApproachAndWait", actor.name
    end
  })
  local condition_func = function()
    return done
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "ActorForceApproachAndWait (Wait)", actor.name
    end
  })
end
function CineSequence:ActorSync(actor, arg_table)
  local slave_list = arg_table.Slaves
  local branch_name = arg_table.Branch
  local relative = arg_table.Relative or true
  local joint_name = arg_table.ReferenceJoint
  local reference_on = arg_table.ReferenceJointOn
  local reference_tween_time = arg_table.ReferenceJointTweenTime
  local reference_object = arg_table.ReferenceObject
  local damping_level = arg_table.DampingLevel
  local puppeteer
  local start_sync = function()
    puppeteer = actor:StartPuppetingForce(reference_object or self.level_obj)
    local slaveTable = {}
    if slave_list then
      for _, slaveInfo in ipairs(slave_list) do
        if slaveInfo.Obj then
          slaveTable[#slaveTable + 1] = {
            Slave = slaveInfo.Obj,
            Anim = slaveInfo.Anim,
            SimpleObjectAnimTweenTime = slaveInfo.TweenTime
          }
        else
          local slavePuppet = slaveInfo.Actor:StartPuppetingForce(reference_object or self.level_obj)
          slavePuppet:AcceptSync()
          if slavePuppet.GetType then
            slavePuppet:SetType(puppeteer:GetType())
          end
          slaveTable[#slaveTable + 1] = {
            Slave = slavePuppet,
            Branch = slaveInfo.Branch or branch_name,
            ForceStartTime = slaveInfo.StartTime
          }
        end
      end
    end
    puppeteer:Sync(branch_name, relative, slaveTable, joint_name, reference_on, reference_tween_time, damping_level)
  end
  self:AddSequencePoint({
    callback_func = start_sync,
    debug_func = function()
      return "ActorSync", actor.name
    end
  })
  local frameCount = 0
  local condition_func = function()
    frameCount = frameCount + 1
    return 1 < frameCount
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "ActorSync (wait a frame - bugfix)", actor.name
    end
  })
end
function CineSequence:ActorEnterTraversePath(actor, arg_table)
  local path_object = arg_table.Path
  local puppeteer
  local callback_func = function()
    puppeteer = actor:StartPuppetingForce(self.level_obj)
    puppeteer:EnterTraversePath(path_object)
  end
  self:AddSequencePoint({
    callback_func = callback_func,
    debug_func = function()
      return "ActorEnterTraversePath", actor.name
    end
  })
end
function CineSequence:WaitForActorPuppeteerComplete(actor)
  local done = false
  local puppeteer
  local on_complete = function()
    done = true
  end
  local start_wait = function()
    puppeteer = actor:GetActivePuppeteer()
    if not puppeteer then
      return error("Actor has no puppeteer")
    end
    puppeteer:OnComplete(on_complete)
  end
  self:AddSequencePoint({
    callback_func = start_wait,
    debug_func = function()
      return "WaitForPuppeteerComplete", actor.name
    end
  })
  local condition_func = function()
    return done
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForPuppeteerComplete (Wait)", actor.name
    end
  })
end
function CineSequence:WaitForActorMoveComplete(actor)
  local done = false
  local puppeteer
  local Set_Done_True = function()
    done = true
  end
  local start_wait = function()
    puppeteer = actor:GetActivePuppeteer()
    if not puppeteer then
      puppeteer = actor:StartPuppetingForce(self.level_obj)
    end
    puppeteer:MonitorPlayingMove()
    puppeteer:OnComplete(Set_Done_True)
  end
  self:AddSequencePoint({
    callback_func = start_wait,
    debug_func = function()
      return "WaitForActorMoveComplete", actor.name
    end
  })
  local condition_func = function()
    return done
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForActorMoveComplete (Wait)", actor.name
    end
  })
end
function CineSequence:WaitForActorFinishMove(actor, moveName)
  local wait_for_playing_move = function()
    return actor:GetCreature():IsPlayingMove(moveName)
  end
  local wait_for_not_playing_move = function()
    return not actor:GetCreature():IsPlayingMove(moveName)
  end
  self:AddWaitSequencePoint({
    condition_func = wait_for_playing_move,
    debug_func = function()
      return "WaitForActorPlayingMove (Wait)", actor.name, moveName
    end
  })
  self:AddWaitSequencePoint({
    condition_func = wait_for_not_playing_move,
    debug_func = function()
      return "WaitForActorNotPlayingMove (Wait)", actor.name, moveName
    end
  })
end
function CineSequence:WaitForActorPlayingMove(actor, moveName)
  local condition_func = function()
    return actor:GetCreature():IsPlayingMove(moveName)
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForActorPlayingMove (Wait)", actor.name, moveName
    end
  })
end
function CineSequence:WaitForActorNotPlayingMove(actor, moveName)
  local condition_func = function()
    return not actor:GetCreature():IsPlayingMove(moveName)
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForActorNotPlayingMove (Wait)", actor.name, moveName
    end
  })
end
function CineSequence:DespawnActor(actor)
  local callback_func = function()
    actor:Despawn()
  end
  self:AddSequencePoint({
    callback_func = callback_func,
    debug_func = function()
      return "DespawnActor", actor.name
    end
  })
end
function CineSequence:StopPuppetingActor(actor)
  local callback_func = function()
    actor:StopPuppeting()
  end
  self:AddSequencePoint({
    callback_func = callback_func,
    debug_func = function()
      return "StopPuppetingActor", actor.name
    end
  })
end
function CineSequence:WaitUntilActorAnimPastFrame(actor, frame_number)
  local condition_func = function()
    local creature = actor:GetCreature()
    return creature.AnimFrame >= frame_number
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitUntilActorAnimPastFrame (Wait)", actor.name .. " " .. actor:GetCreature().AnimFrame
    end
  })
end
function CineSequence:WaitUntilActorAnimPastPercentage(actor, percentage)
  local condition_func = function()
    local creature = actor:GetCreature()
    if 1 < percentage then
      percentage = percentage / 100
    end
    return creature.AnimFrame / creature.AnimLengthFrames >= percentage
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitUntilActorAnimPastPercentage (Wait)", actor.name .. " " .. actor:GetCreature().AnimFrame / actor:GetCreature().AnimLengthFrames
    end
  })
end
function CineSequence:WaitUntilLuaObjectAnimPastPercentage(luaObject, percentage)
  local condition_func = function()
    if 1 < percentage then
      percentage = percentage / 100
    end
    if luaObject.IsRefnode then
      luaObject = luaObject.Child
    end
    return luaObject.AnimFrame / luaObject.AnimLengthFrames >= percentage
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitUntilLuaObjectAnimPastPercentage (Wait)", luaObject:GetName() .. " " .. luaObject.AnimFrame / luaObject.AnimLengthFrames
    end
  })
end
function CineSequence:RequestCineModeAndWait(actor, obj, optionalSkipCineMode)
  if not actor or not obj then
    return error("Must specify Actor and Object for the interaction")
  end
  local creature
  local lock_select_inputs = function()
    local pad = game.Player.FindPlayer().Pad
    pad:DisableGameButton(tweaks.ePad.kPadUp)
  end
  local story_trophy_fixup = function()
    local currentCineNum = game.Level.GetVariable("CompletedCineNumber")
    local story_trophy_list = {
      {trophy = 1, cineNum = 100},
      {trophy = 2, cineNum = 165},
      {trophy = 3, cineNum = 245},
      {trophy = 4, cineNum = 290},
      {trophy = 5, cineNum = 360},
      {trophy = 6, cineNum = 380},
      {trophy = 7, cineNum = 390},
      {trophy = 8, cineNum = 492},
      {trophy = 9, cineNum = 498},
      {trophy = 10, cineNum = 570},
      {trophy = 11, cineNum = 610}
    }
    for _, story_trophy in ipairs(story_trophy_list) do
      if currentCineNum >= story_trophy.cineNum then
        game.UnlockTrophy(story_trophy.trophy)
      else
        return
      end
    end
  end
  local request_interaction = function()
    creature = actor:GetCreature()
    creature:RequestInteract(obj)
    story_trophy_fixup()
  end
  self:AddSequencePoint({
    callback_func = request_interaction,
    debug_func = function()
      return "ActorRequestInteraction", actor.name
    end
  })
  local condition_func = function()
    creature = actor:GetCreature()
    if creature:IsInteracting() and creature:GetCurrentInteractObject() == obj then
      if not optionalSkipCineMode then
        game.Cinematics.EnableCinematicMode()
        game.Player.FindPlayer():CallScript("LuaHook_ForceCineRageModeExit")
        local son = game.AI.FindSon()
        if son ~= nil then
          son:CallScript("LuaHook_ClearSummons")
        end
        lock_select_inputs()
      end
      return true
    elseif creature:IsInteracting() and creature:GetCurrentInteractObject() ~= obj then
      engine.Warning("Warning: cine sequence " .. tostring(self.name) .. " likely will fail because actor " .. tostring(creature) .. " is interacting but not with the cine's chosen object. Current nteraction object is: " .. tostring(creature:GetCurrentInteractObject()) .. " but intended interaction object is: " .. tostring(obj))
      return false
    else
      return false
    end
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "InteractApproval_StartCineMode (Wait)", actor.name
    end
  })
end
function CineSequence:WaitForAndCompleteCineMode(actor, obj)
  local creature
  local unlock_select_inputs = function()
    local pad = game.Player.FindPlayer().Pad
    pad:EnableGameButton(tweaks.ePad.kPadUp)
  end
  local condition_func = function()
    creature = actor:GetCreature()
    if not creature then
      return error("Actor has no creature to poll for interaction")
    end
    return creature:GetCurrentInteractObject() ~= obj
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForActorInteractComplete (Wait)", actor.name
    end
  })
  local disable_cinematic_mode = function()
    game.Cinematics.DisableCinematicMode()
    unlock_select_inputs()
    if game.Compass.OnWarp then
      game.Compass.OnWarp()
    end
  end
  self:AddSequencePoint({
    callback_func = disable_cinematic_mode,
    debug_func = function()
      return "Disable Cinematic Mode"
    end
  })
end
function CineSequence:StartPOIMoment(speed)
  local player = game.Player.FindPlayer()
  if self.buttonsMasked and self.buttonsMasked ~= {} then
    engine.Error("Error: You must end a POI moment before starting a new one.")
  end
  self.buttonsMasked = {}
  local TryToDisableButton = function(button)
    if not player.Pad:IsGameButtonDisabled(button) then
      self.buttonsMasked[#self.buttonsMasked + 1] = button
      player.Pad:DisableGameButton(button)
    end
  end
  local callback_func_zone_input = function()
    if speed ~= nil then
      player:CallScript("SpeedControl_AddZone", self.level_obj, speed)
    end
    TryToDisableButton(tweaks.ePad.kPadL2)
    TryToDisableButton(tweaks.ePad.kPadR2)
    TryToDisableButton(tweaks.ePad.kPadL1)
    TryToDisableButton(tweaks.ePad.kPadR1)
    TryToDisableButton(tweaks.ePad.kPadTriangle)
    TryToDisableButton(tweaks.ePad.kPadCross)
    TryToDisableButton(tweaks.ePad.kPadSquare)
    TryToDisableButton(tweaks.ePad.kPadL3)
    TryToDisableButton(tweaks.ePad.kPadUp)
    TryToDisableButton(tweaks.ePad.kPadRight)
    TryToDisableButton(tweaks.ePad.kPadDown)
    TryToDisableButton(tweaks.ePad.kPadLeft)
  end
  self:AddSequencePoint({
    callback_func = callback_func_zone_input,
    debug_func = function()
      return "StartPOIMoment (ZoneInput)", speed
    end
  })
end
function CineSequence:EndPOIMoment()
  local callback_func_zone_input = function()
    local player = game.Player.FindPlayer()
    player:CallScript("SpeedControl_RemoveZone", self.level_obj)
    for _, button in ipairs(self.buttonsMasked) do
      player.Pad:EnableGameButton(button)
    end
    self.buttonsMasked = {}
  end
  self:AddSequencePoint({
    callback_func = callback_func_zone_input,
    debug_func = function()
      return "EndPOIMoment"
    end
  })
end
function CineSequence:WaitForCompletedCineNumber(number)
  local condition_func = function()
    return game.Level.GetVariable("CompletedCineNumber") < number
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForCompletedCineNumber", string.format("%d => %d", game.Level.GetVariable("CompletedCineNumber"), number)
    end
  })
end
function CineSequence:SetCompletedCineNumber(number)
  local callback_func = function()
    game.Level.SetVariable("CompletedCineNumber", number)
    if game.Cinematics.AlertCinematicViewed ~= nil then
      game.Cinematics.AlertCinematicViewed(self.name)
    end
  end
  self:AddSequencePoint({
    callback_func = callback_func,
    debug_func = function()
      return "SetCompletedCineNumber", number
    end
  })
end
function CineSequence:BeginMultipleWait()
  if self.multiple_wait_list then
    return error("Call to BeginMultipleWait inside existing multiple wait")
  end
  self.multiple_wait_list = {}
end
function CineSequence:EndMultipleWait()
  if not self.multiple_wait_list then
    return error("Call to EndMultipleWait without call to BeginMultipleWait")
  end
  local multiple_wait_list = self.multiple_wait_list
  self.multiple_wait_list = nil
  local local_elapsed_time = 0
  local condition_func = function()
    local dt = self.level:GetUnitTime()
    local_elapsed_time = local_elapsed_time + dt
    return run_sequence(multiple_wait_list, 1, local_elapsed_time, self.skipping)
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "MultipleWait"
    end,
    multiple_wait_list = multiple_wait_list
  })
end
function CineSequence:WaitFrames(n)
  local frameCount = 0
  local condition_func = function()
    if self.level:GetUnitTime() > 0 then
      frameCount = frameCount + 1
    end
    return frameCount > n
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitFrames", math.max(0, n - frameCount)
    end
  })
end
function CineSequence:WaitSeconds(seconds)
  local timeWaiting = 0
  local condition_func = function()
    timeWaiting = timeWaiting + self.current_dt
    return timeWaiting >= seconds
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitSeconds", math.max(0, seconds - timeWaiting)
    end
  })
end
function CineSequence:WaitForFunctionTrue(condition_func, ...)
  local func = condition_func
  local args = (...) and {
    ...
  }
  if args then
    function func()
      return condition_func(unpack(args))
    end
  end
  self:AddWaitSequencePoint({
    condition_func = func,
    debug_func = function()
      if args then
        return "WaitForFunctionTrue", find_symbol_name(func), unpack(args)
      else
        return "WaitForFunctionTrue", find_symbol_name(func)
      end
    end
  })
end
function CineSequence:WaitForSyncStart(creature)
  local condition_func = function()
    return creature:IsDoingSyncMove()
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForSyncStart"
    end
  })
end
function CineSequence:WaitForLoadCheck(wadname)
  local condition_func = function()
    return game.UI.LoadCheck(wadname) == 0 and game.FindLevel(wadname) ~= nil
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "LoadCheck (wait)", wadname
    end
  })
end
function CineSequence:WaitForSpawn()
  local frameCount = 0
  local condition_func = function()
    frameCount = frameCount + 1
    return 1 < frameCount
  end
  self:AddWaitSequencePoint({
    condition_func = condition_func,
    debug_func = function()
      return "WaitForSpawn"
    end
  })
end
function CineSequence:ParentObject(child, parent, jointName)
  local callback_func = function()
    parent:AddChild(child, parent:GetJointIndex(jointName))
  end
  self:AddSequencePoint({
    callback_func = callback_func,
    debug_func = function()
      return "ParentObject", child, parent, jointName
    end
  })
end
function CineSequence:UnparentObject(child)
  local callback_func = function()
    child:Unparent()
  end
  self:AddSequencePoint({
    callback_func = callback_func,
    debug_func = function()
      return "UnparentObject", child
    end
  })
end
return {CineSequence = CineSequence}
