local LD = require("design.LevelDesignLibrary")
local uiCalls = require("ui.uicalls")
local timers, TUT, thisObj, interactZone
local interactLocked = false
local player, son
local runePuzzleSolved = false
local chiselObject
local destroyInProgress = false
local chiselBreakableChild, chiselBlocker_Sibling, chiselModuleParent, doorObj, bloodRune
local interacting = false
local sonContextActionObj, gearCheckPassed, sonNextoChisel, hasPlayedOnce, markerFX, gamePlanePoint, gameX, gameY, mouseX, mouseY, leftStickXDriver, leftStickYDriver
local targetsToFind = 3
local chiselTargets = {}
local startDistanceOfMatEffect = 0.35
local thresholdOfSuccess = 0.25
local minimumDistanceBetweenTargets = 0.25
local timeToFadeAway = 15
local runeFadeAnimFrameLength = 5
local gameX_cached, gameY_cached, indexOfClosestTarget, adjustedLength, adjustedDistance, materialAnimRate
local maxAlphaBlendForNotSuccess = 90
local currentRumble, currentRumbleTime, heavyLoopOn, lightLoopOn
local padLightSuccessColor = 16711680
local padLightFarColor = 16750336
local successIndicator, successIndicator_blocker
local IsTutorial = false
local BanterIsAvailable = false
local BanterQueue, TroubleFindingSpotTimer, HoverOverSuccessTimer
local GlobalBanterCooldown = 3
local TroubleFindingSpotTime = 30
local HoverOverSuccessTime = 1.15
local HoverOverSuccessCooldown = 12
local HoverOverSuccessOnCooldown = false
local chiselInteractOnStart, chiselInteractOnFinish, onBlackRuneInteractStart
local triggerCinematic = false
local cineInteractionTriggers, difficulty
local CallbackQueue_RuneIntroComplete = {}
local CallbackQueue_StartRuneFadeAnimation = {}
local CallbackQueue_RuneHasFaded = {}
local OnSonStartRunePOI, OnPlayerStartChiselGame, OnPlayerExitChiselGame
local DifficultyTable = {
  VeryEasy = {
    targetsToFind = 2,
    thresholdOfSuccess = 0.3,
    startDistanceOfMatEffect = 0.45,
    timeToFadeAway = 25,
    random = true
  },
  Easy = {
    targetsToFind = 2,
    thresholdOfSuccess = 0.25,
    startDistanceOfMatEffect = 0.35,
    timeToFadeAway = 25,
    random = true
  },
  Medium = {
    targetsToFind = 3,
    thresholdOfSuccess = 0.2,
    startDistanceOfMatEffect = 0.35,
    timeToFadeAway = 15,
    random = true
  },
  Hard = {
    targetsToFind = 4,
    thresholdOfSuccess = 0.2,
    startDistanceOfMatEffect = 0.35,
    timeToFadeAway = 15,
    random = true
  },
  BlackRuneCine = {
    targetsToFind = 4,
    thresholdOfSuccess = 0.2,
    startDistanceOfMatEffect = 0.35,
    timeToFadeAway = 15,
    random = false,
    locations = {
      {x = -0.7, y = 0.07},
      {x = 0.5, y = -0.5},
      {x = 0.71, y = -0.03},
      {x = -0.55, y = 0.55}
    },
    sonDisplayedBlood = false
  }
}
local cineSyncObject
local TUT_LoadLibrary = function()
  if TUT == nil then
    TUT = require("game.GlobalTutorials")
  end
end
local timers_LoadLibrary = function()
  if timers == nil then
    timers = require("level.timer")
  end
end
function OnScriptLoaded(level, obj)
  mouseX = 0
  mouseY = 0
  player = game.Player.FindPlayer()
  son = game.AI.FindSon()
  thisObj = obj
  difficulty = thisObj:GetLuaTableAttribute("ChiselGameConfig") or "Medium"
  local diffInfo = DifficultyTable[difficulty]
  targetsToFind = diffInfo.targetsToFind
  thresholdOfSuccess = diffInfo.thresholdOfSuccess
  minimumDistanceBetweenTargets = diffInfo.thresholdOfSuccess
  startDistanceOfMatEffect = diffInfo.startDistanceOfMatEffect
  timeToFadeAway = diffInfo.timeToFadeAway
  if level.Name == "WAD_Stn750_ReturnHallway" and game.GetMiniGameplaySkipped() == false then
    IntializeTutorialBanter()
  end
  if difficulty == "BlackRuneCine" then
    bloodRune = thisObj:FindSingleGOByName("bloodRune")
    onBlackRuneInteractStart = thisObj:FindLuaTableAttribute("OnStartInteractCallback")
    onBlackRuneInteractStart = LD.ExtractCallbacksForEvent(level, thisObj, onBlackRuneInteractStart)
    OnSonStartRunePOI = thisObj:FindLuaTableAttribute("OnSonStartRunePOI")
    OnSonStartRunePOI = LD.ExtractCallbacksForEvent(level, thisObj, OnSonStartRunePOI)
  end
  if IsBlackRuneCineActive() then
    cineSyncObject = thisObj:FindSingleGOByName("CineSyncObject")
    OnPlayerStartChiselGame = thisObj:FindLuaTableAttribute("OnPlayerStartChiselGame")
    OnPlayerStartChiselGame = LD.ExtractCallbacksForEvent(level, thisObj, OnPlayerStartChiselGame)
    OnPlayerExitChiselGame = thisObj:FindLuaTableAttribute("OnPlayerExitChiselGame")
    OnPlayerExitChiselGame = LD.ExtractCallbacksForEvent(level, thisObj, OnPlayerExitChiselGame)
  else
    chiselBreakableChild = obj:FindSingleGOByName("bowl_w_stand_01")
  end
  if difficulty ~= "BlackRuneCine" and obj.Parent.Parent ~= nil then
    chiselModuleParent = obj.Parent.Parent
  end
  sonContextActionObj = thisObj:FindSingleGOByName("CA_Observe_Forward")
  if chiselModuleParent then
    if string.lower(thisObj.Parent:GetName()) == "chiselbreakable_front" then
      chiselBlocker_Sibling = chiselModuleParent:FindSingleGOByName("chiselBreakable_Back")
    elseif string.lower(thisObj.Parent:GetName()) == "chiselbreakable_back" then
      chiselBlocker_Sibling = chiselModuleParent:FindSingleGOByName("chiselBreakable_Front")
    end
    local doorObjectName = chiselModuleParent:GetLuaTableAttribute("DoorObjectName")
    doorObj = chiselModuleParent:FindSingleGOByName(doorObjectName)
    if doorObj.IsRefNode then
      doorObj = doorObj.Child
    end
    chiselInteractOnStart = chiselModuleParent:FindLuaTableAttribute("EventOnStart")
    chiselInteractOnFinish = chiselModuleParent:FindLuaTableAttribute("EventOnFinish")
    cineInteractionTriggers = chiselModuleParent:FindLuaTableAttribute("cineInteractionTriggers")
    triggerCinematic = false
    chiselInteractOnStart = LD.ExtractCallbacksForEvent(level, thisObj, chiselInteractOnStart)
    chiselInteractOnFinish = LD.ExtractCallbacksForEvent(level, thisObj, chiselInteractOnFinish)
    cineInteractionTriggers = LD.ExtractCallbacksForEvent(level, thisObj, cineInteractionTriggers)
  elseif difficulty ~= "BlackRuneCine" then
    engine.Warning("GameObject: '", thisObj.Parent:GetName(), "' in level '", level.Name, "' is refnoded directly in level, instead of using a ChiselDoor module (", LD.GetParentTrace(thisObj), ")")
  end
  interactZone = LD.CreateInteractZone_Standard_180(obj, "promptJoint")
  interactZone:SetRequiresSonUnoccupied(true)
  interactZone:SetHintAngle(180)
  LD.OverrideInteractZoneTags(interactZone, "NotWhileSonInteracting", true)
  LD.OverrideInteractZoneTags(interactZone, "NotWhileSonAtSandbowl", true)
  markerFX = {
    {
      fx = thisObj:FindSingleGOByName("RuneAnimated_1").Child,
      z = 0,
      visible = false,
      chiselTargetsIdx = -1
    },
    {
      fx = thisObj:FindSingleGOByName("RuneAnimated_2").Child,
      z = 0,
      visible = false,
      chiselTargetsIdx = -1
    },
    {
      fx = thisObj:FindSingleGOByName("RuneAnimated_3").Child,
      z = 0,
      visible = false,
      chiselTargetsIdx = -1
    },
    {
      fx = thisObj:FindSingleGOByName("RuneAnimated_4").Child,
      z = 0,
      visible = false,
      chiselTargetsIdx = -1
    }
  }
  for _, FX in ipairs(markerFX) do
    FX.fx:PauseAnimation()
    FX.fx:Hide()
  end
  for i = 1, targetsToFind do
    local newTarget
    if diffInfo.random then
      local px, py = GetRandomPointInSquare(startDistanceOfMatEffect, 1)
      newTarget = {
        x = px,
        y = py,
        active = true
      }
      local newTargetTooClose = true
      while newTargetTooClose do
        newTargetTooClose = false
        for _, t in ipairs(chiselTargets) do
          local distanceSquared = (newTarget.x - t.x) * (newTarget.x - t.x) + (newTarget.y - t.y) * (newTarget.y - t.y)
          if distanceSquared <= minimumDistanceBetweenTargets * minimumDistanceBetweenTargets then
            newTarget.x, newTarget.y = GetRandomPointInSquare(startDistanceOfMatEffect, 1)
            newTargetTooClose = true
          end
        end
      end
    else
      local storageVector = engine.Vector.New(diffInfo.locations[i].x, 0, diffInfo.locations[i].y)
      storageVector = storageVector:RotateXZ(45)
      storageVector.x, storageVector.z = MapCoordinatesOnCircleToSquare(storageVector.x, storageVector.z)
      storageVector = storageVector:RotateXZ(-45)
      newTarget = {
        x = storageVector.x,
        y = storageVector.z,
        active = true
      }
    end
    table.insert(chiselTargets, newTarget)
  end
  startDistanceOfMatEffect = startDistanceOfMatEffect * startDistanceOfMatEffect
  thresholdOfSuccess = thresholdOfSuccess * thresholdOfSuccess
  SoundOnInit()
  game.SubObject.Sleep(thisObj)
end
function OnFirstStart()
  if bloodRune then
    bloodRune:Hide()
  end
end
function OnStart()
  if runePuzzleSolved then
    interactZone:Disable()
    ContextActionIsEnabled(false)
  else
    if not PlayerHasGear() then
      ContextActionIsEnabled(true)
    end
    if interactLocked == true and not PlayerHasGear() then
      Lock()
    else
      Unlock()
      if IsBlackRuneCineActive() then
        if not DifficultyTable[difficulty].sonDisplayedBlood then
          interactZone:Disable()
        end
        if sonContextActionObj ~= nil then
          sonContextActionObj.LuaObjectScript.Disable()
        end
      end
    end
  end
end
function ResolveSoftState()
  if runePuzzleSolved then
    LD.CallFunctionAfterDelay(function()
      thisObj:Hide()
    end, 0.1)
  end
end
function SoftSave()
  game.SubObject.SoftSave(thisObj)
end
function IsPuzzleSolved()
  return runePuzzleSolved
end
function Unlock()
  interactLocked = false
  interactZone:Unlock()
end
function Lock()
  interactLocked = true
  interactZone:Lock()
end
function OnSaveCheckpoint(level, obj)
  return {
    runePuzzleSolved = runePuzzleSolved,
    interactLocked = interactLocked,
    sonDisplayedBlood = DifficultyTable.BlackRuneCine.sonDisplayedBlood
  }
end
function OnRestoreCheckpoint(level, obj, savedInfo)
  runePuzzleSolved = savedInfo.runePuzzleSolved
  interactLocked = savedInfo.interactLocked
  DifficultyTable.BlackRuneCine.sonDisplayedBlood = savedInfo.sonDisplayedBlood
end
function StartSonInteraction()
  if PlayerHasGear() then
    gearCheckPassed = true
  end
  son:RequestInteract(thisObj)
end
function OnUseWorld(level, obj)
  if interactZone:PlayerCanInteract() then
    if cineInteractionTriggers then
      LD.ExecuteCallbacksForEvent(level, thisObj, cineInteractionTriggers, "Cine Interaction Event")
    end
    if triggerCinematic == false then
      if PlayerHasGear() then
        gearCheckPassed = true
        player:RequestInteract(thisObj, {interactZone = interactZone})
      else
        player:RequestInteract(thisObj, {interactZone = interactZone})
      end
    end
    triggerCinematic = false
  end
end
function OnInteractStart(level, obj)
  if gearCheckPassed then
    PerformMainInteract()
  else
    PerformFirstTimeInteract()
  end
  if difficulty == "BlackRuneCine" and onBlackRuneInteractStart then
    LD.ExecuteCallbacksForEvent(level, thisObj, onBlackRuneInteractStart, "On Black Rune Interaction Start")
  end
end
function OnInteractDone()
  if gearCheckPassed then
    ChiselInteractDone()
    if son.OwnedPOI ~= nil then
      CancelSonContextAction()
      timers_LoadLibrary()
      timers.StartLevelTimer(1, function()
        ContextActionIsEnabled(false)
      end)
    end
  else
    Lock()
    CancelSonContextAction()
    timers_LoadLibrary()
    timers.StartLevelTimer(1, function()
      ContextActionIsEnabled(false)
    end)
  end
end
function OnUpdate(level, obj)
  if gearCheckPassed then
    UpdateChiselSpawn()
    if interacting then
      gamePlanePoint = GetGamePlanePointClosestToChisel()
      GetInput()
      UpdateChisel(level, obj)
    end
    if runePuzzleSolved and thisObj ~= player:GetCurrentInteractObject() then
      game.SubObject.Sleep(thisObj)
      if doorObj then
        doorObj.LuaObjectScript.Enable()
        doorObj.LuaObjectScript.ForceOnInteractStart(string.lower(thisObj.Parent:GetName()) == "chiselbreakable_back", "BRA_DoorSpreadOpen_Chisel")
      end
    end
  end
end
function UpdateChisel(level, obj)
  local minSquareDistance = 10
  for i = 1, #chiselTargets do
    if chiselTargets[i].active then
      local sqrDistance = (chiselTargets[i].x - gameX) * (chiselTargets[i].x - gameX) + (chiselTargets[i].y - gameY) * (chiselTargets[i].y - gameY)
      if minSquareDistance > sqrDistance then
        minSquareDistance = sqrDistance
        indexOfClosestTarget = i
      end
    end
  end
  if chiselObject and chiselObject.Spawned then
    if minSquareDistance <= thresholdOfSuccess then
      UpdateSoundProximityRTPC(1)
      LD.SetAnimatedMaterialAttributes(chiselObject, "chiselShard00", "gameplayLighten", {AlphaBlend = 100})
      if not heavyLoopOn then
        SubmitRumble("FFB_LOOP_LARGE")
        StopRumble("FFB_LOOP_SMALL")
        heavyLoopOn = true
        lightLoopOn = false
      end
      game.Camera.SubmitCameraByName("PLYR_Interact_Chisel_Stab_Hint")
      player.Pad:SetLightColor(padLightSuccessColor)
      if successIndicator then
        successIndicator:Show()
      end
      if not successIndicator_blocker then
        successIndicator_blocker = game.FX.Spawn("chisel_successIndicator", nil, {AutoDelete = false})
      end
      if IsTutorial then
        TriggerHoverOverSuccessTimer()
        StopTutorialTroubleFindingTimer()
      end
      successIndicator_blocker:SetWorldPosition(gamePlanePoint)
    elseif minSquareDistance <= startDistanceOfMatEffect then
      adjustedLength = startDistanceOfMatEffect - thresholdOfSuccess
      adjustedDistance = minSquareDistance - thresholdOfSuccess
      materialAnimRate = (1 - adjustedDistance / adjustedLength) * maxAlphaBlendForNotSuccess
      LD.SetAnimatedMaterialAttributes(chiselObject, "chiselShard00", "gameplayLighten", {AlphaBlend = materialAnimRate})
      if not lightLoopOn then
        SubmitRumble("FFB_LOOP_SMALL")
        StopRumble("FFB_LOOP_LARGE")
        lightLoopOn = true
        heavyLoopOn = false
      end
      player.Pad:SetLightColor(InteroplateColor(padLightFarColor, padLightSuccessColor, materialAnimRate / 100))
      RemoveSuccessIndicator()
      UpdateSoundProximityRTPC(materialAnimRate / 100)
      if IsTutorial then
        StopHoverOverSuccessTimer()
        TriggerTutorialTroubleFindingTimer()
      end
    else
      LD.SetAnimatedMaterialAttributes(chiselObject, "chiselShard00", "gameplayLighten", {AlphaBlend = 0})
      StopRumble("FFB_LOOP_SMALL")
      StopRumble("FFB_LOOP_LARGE")
      heavyLoopOn = false
      lightLoopOn = false
      player.Pad:SetLightColor(padLightFarColor)
      RemoveSuccessIndicator()
      UpdateSoundProximityRTPC(0)
      if IsTutorial then
        StopHoverOverSuccessTimer()
        TriggerTutorialTroubleFindingTimer()
      end
    end
  end
end
function UpdateChiselSpawn()
  if chiselObject and chiselObject.Spawned and not successIndicator then
    successIndicator = chiselObject:FindSingleGOByName("ChiselEnableFx")
    successIndicator:Hide()
  end
end
function GetInput()
  if player.Pad.IsKeyboardAndMouse then
    mouseX = math.min(math.max(mouseX + player.Pad.DampedRightStick.x * player.Pad.GetChiselSpeed, -1), 1)
    mouseY = math.min(math.max(mouseY + player.Pad.DampedRightStick.y * player.Pad.GetChiselSpeed, -1), 1)
    gameX = mouseX
    gameY = mouseY
  else
    gameX = player.Pad.DampedLeftStick.x
    gameY = player.Pad.DampedLeftStick.y
  end
  gameY = -gameY
  local storageVector = engine.Vector.New(gameX, 0, gameY)
  storageVector = storageVector:RotateXZ(45)
  storageVector.x, storageVector.z = MapCoordinatesOnCircleToSquare(storageVector.x, storageVector.z)
  storageVector = storageVector:RotateXZ(-45)
  gameX, gameY = storageVector.x, storageVector.z
  leftStickXDriver.Value = gameX
  leftStickYDriver.Value = -gameY
  if engine.IsDebug() then
    local cheatWin = player.Pad.R1Down and player.Pad.R2Down and player.Pad.R3Down
    if cheatWin then
      TUT_LoadLibrary()
      TUT.HideTutorial()
      runePuzzleSolved = true
      CallbackQueue_RuneIntroComplete = {}
      CallbackQueue_StartRuneFadeAnimation = {}
      CallbackQueue_RuneHasFaded = {}
      RemoveSuccessIndicator()
      player:TriggerMoveEvent("LE_ChiselGameFinished")
      DestroyChiselBlocker()
    end
  end
end
function PerformMainInteract()
  if IsBlackRuneCineActive() and not DifficultyTable[difficulty].sonDisplayedBlood then
    game.Interact.DisableTags("NotWhileSonInteracting")
    local bow, quiver, knife
    for weaponInfo in son:IterateActiveWeapons() do
      local gear = weaponInfo.Weapon
      if gear:GetName() == "quiver00" then
        quiver = gear
      elseif gear:GetName() == "knife00" then
        knife = gear
      elseif gear:GetName() == "bow00" then
        bow = gear
      end
    end
    local syncTable = {
      {
        son,
        "BRA_PeakBaldurReturns"
      },
      {bow},
      {quiver},
      {knife}
    }
    LD.PlayApproach_GroupSync(cineSyncObject, syncTable, nil, true, "synchJoint")
    if OnSonStartRunePOI then
      LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, OnSonStartRunePOI)
    end
    return
  end
  game.SubObject.Wake(thisObj)
  uiCalls.UI_Global_Menu_Set_Exclusive_Access("GlobalInWorldMenu")
  uiCalls.UI_Journal_Log_Pause()
  leftStickXDriver = player:GetAnimDriver("DampingLeftStickX_Chisel")
  leftStickYDriver = player:GetAnimDriver("DampingLeftStickY_Chisel")
  leftStickXDriver.Value = 0
  leftStickYDriver.Value = 0
  if IsBlackRuneCineActive() then
    if game.GetMiniGameplaySkipped() == true then
      LD.PlaySingleSynchMove_KratosObject(thisObj, "synchJoint", "Chisel Mini Game", "BRA_EnvChiselGameEnterSingleButtonPress", "", interactZone, false, "BareOnBack")
      if OnPlayerStartChiselGame then
        LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, OnPlayerStartChiselGame)
      end
    else
      LD.PlaySingleSynchMove_KratosObject(thisObj, "synchJoint", "Chisel Mini Game", "BRA_EnvChiselGameEnter", "", interactZone, false, "Bare")
      if OnPlayerStartChiselGame then
        LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, OnPlayerStartChiselGame)
      end
    end
  else
    if IsTutorial then
      BanterIsAvailable = false
      game.Audio.PlayBanter("stn_ChiselTutorialIntro", IntializeTutorialBanter)
    end
    player:MarkCurrentWeaponMode()
    if game.GetMiniGameplaySkipped() == true then
      LD.PlaySingleSynchMove_KratosObject(thisObj, "synchJoint", "Chisel Mini Game", "BRA_EnvChiselGameEnterSingleButtonPress", "", interactZone, false, "Bare")
    else
      LD.PlaySingleSynchMove_KratosObject(thisObj, "synchJoint", "Chisel Mini Game", "BRA_EnvChiselGameEnter", "", interactZone, false, "Bare")
    end
  end
  if game.GetMiniGameplaySkipped() == false then
    TUT_LoadLibrary()
    TUT.ChiselDoor_Tutorial()
  end
  game.Camera.Recenter({
    TimeStart = 0,
    TimeDuration = 2,
    LockRecenter = 0,
    YawRange = 0,
    TriggerLeft = 0,
    TriggerRight = 0,
    ReturnLeft = 0,
    ReturnRight = 0,
    PitchRange = 0,
    ReturnUp = -8,
    ReturnDown = -8
  })
  local availabilityState = {AvailableForCombat = false}
  son:SetNewAvailabilityRequest("LevelDesignScript", availabilityState)
end
function ChiselInteractDone()
  uiCalls.UI_Global_Menu_Clear_Exclusive_Access("GlobalInWorldMenu")
  uiCalls.UI_Journal_Log_Unpause()
  if IsBlackRuneCineActive() and not DifficultyTable[difficulty].sonDisplayedBlood then
    DifficultyTable[difficulty].sonDisplayedBlood = true
    interactZone:Enable()
    GameObjects.Peak740_Banter.LuaObjectScript.BlackRuneB_Banter()
    game.Interact.EnableTags("NotWhileSonInteracting")
    return
  end
  if runePuzzleSolved == false then
    EndMiniGameInteracting(true)
    game.SubObject.Sleep(thisObj)
    if OnPlayerExitChiselGame then
      LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, OnPlayerExitChiselGame)
    end
  elseif runePuzzleSolved then
    if chiselInteractOnFinish then
      LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, chiselInteractOnFinish)
    end
    if IsTutorial then
      QueueBanter("stn_ChiselTutorialSuccess")
    end
    if difficulty ~= "BlackRuneCine" and chiselModuleParent ~= nil then
      chiselModuleParent.LuaObjectScript.SoftSave()
    end
  end
  StopSoundOnInteractEnd()
  TUT_LoadLibrary()
  TUT.HideTutorial()
  son:RemoveAvailabilityRequest("LevelDesignScript")
  if HoverOverSuccessTimer ~= nil then
    HoverOverSuccessTimer:Stop()
    HoverOverSuccessTimer = nil
  end
  if TroubleFindingSpotTimer ~= nil then
    TroubleFindingSpotTimer:Stop()
    TroubleFindingSpotTimer = nil
  end
end
function PerformFirstTimeInteract()
  LD.PlaySingleSynchMove_KratosObject(thisObj, "synchJointMissingGear", "Chisel Blocker First time Interaction", "BRA_DoorBlocked", "", interactZone, false)
end
function LuaHook_TriggerGearLine()
  if not hasPlayedOnce and sonNextoChisel then
    game.Audio.PlayBanterNonCritical("ca_object_magicalsealed", nil, nil, false)
    game.Audio.SetBanterFact("interactedWithChiselDoor", "True")
    hasPlayedOnce = true
    timers_LoadLibrary()
    local exitTimer = timers.StartLevelTimer(2, CancelSonContextAction)
    exitTimer:Start()
  end
end
function ContextActionIsEnabled(value)
  if sonContextActionObj ~= nil then
    if value == true then
      sonContextActionObj.LuaObjectScript.Enable()
    else
      sonContextActionObj.LuaObjectScript.Disable()
    end
  end
end
function CancelSonContextAction()
  if sonContextActionObj ~= nil then
    sonContextActionObj.LuaObjectScript.Interrupt()
  end
end
function PlayerHasGear()
  return game.Wallets.GetResourceValue("HERO", "ChiselUnlock") > 0
end
function SonNextToChisel()
  print("GOT NEXT TO CHISEL")
  if PlayerHasGear() == true then
    interactZone:Unlock()
    ContextActionIsEnabled(false)
  end
  sonNextoChisel = true
  local distToPlayer = LD.GetDistanceBetweenTwoObjects(player, son)
  if distToPlayer <= 8 and interactLocked then
    LuaHook_TriggerGearLine()
  end
end
function SonLeftChisel()
  sonNextoChisel = false
end
function EndMiniGameInteracting(stopRumble)
  DisableInteraction()
  SetGamePadInput(true)
  RemoveSuccessIndicator()
  player.Pad:ResetLightColor()
  if stopRumble == true then
    print("stopping rumble")
    StopRumble("FFB_LOOP_SMALL")
    StopRumble("FFB_LOOP_LARGE")
  end
  HideMarkers()
  for _, target in ipairs(chiselTargets) do
    target.active = true
  end
end
function DestroyChiselBlocker()
  if not destroyInProgress then
    destroyInProgress = true
    interactZone:Disable()
    EndMiniGameInteracting(false)
    runePuzzleSolved = true
    if chiselBlocker_Sibling ~= nil then
      chiselBlocker_Sibling.LuaObjectScript.ImmediatelyDestroyDoor()
    end
    PlayBreakAnimation()
  end
end
function PlayBreakAnimation()
  if not IsBlackRuneCineActive() then
    chiselBreakableChild:GetBreakable():Break()
    PlaySoundOnDoorCollapse()
    for _, marker in pairs(markerFX) do
      marker.fx:ClearAllAnimationCallbacks()
      marker.fx:Hide()
    end
  end
end
function HideMarkers()
  for _, marker in pairs(markerFX) do
    if marker.visible then
      marker.fx:OnAnimationDone(thisObj, "RuneHasFaded", {Force = true})
      marker.fx:PlayAnimationToEnd({Rate = 0.25})
    end
  end
end
function ImmediatelyDestroyDoor()
  if runePuzzleSolved == false then
    DestroyChiselBlocker()
  end
  if doorObj then
    doorObj.LuaObjectScript.Enable()
  end
  if sonContextActionObj ~= nil then
    sonContextActionObj.LuaObjectScript.Interrupt()
    sonContextActionObj.LuaObjectScript.Disable()
  end
end
function RemoveSuccessIndicator()
  if successIndicator then
    successIndicator:Hide()
  end
  if successIndicator_blocker then
    successIndicator_blocker:Remove()
  end
  successIndicator_blocker = nil
end
function LuaHook_KillChisel()
  if chiselObject then
    successIndicator = nil
    chiselObject:Remove()
    chiselObject = nil
    StopSoundOnInteractEnd()
    StopRumble("FFB_LOOP_SMALL")
    StopRumble("FFB_LOOP_LARGE")
  end
end
function RuneIntroComplete(level, go)
  if CallbackQueue_RuneIntroComplete == nil or #CallbackQueue_RuneIntroComplete <= 0 then
    return
  end
  local markerIdx = table.remove(CallbackQueue_RuneIntroComplete, 1)
  local allTargetsFound = true
  for _, target in pairs(chiselTargets) do
    if target.active then
      print(target)
      allTargetsFound = false
    end
  end
  if allTargetsFound or game.GetMiniGameplaySkipped() == true then
    runePuzzleSolved = true
    CallbackQueue_RuneIntroComplete = {}
    CallbackQueue_StartRuneFadeAnimation = {}
    CallbackQueue_RuneHasFaded = {}
    RemoveSuccessIndicator()
    player:TriggerMoveEvent("LE_ChiselGameFinished")
    DestroyChiselBlocker()
    return
  end
  CallbackQueue_StartRuneFadeAnimation[#CallbackQueue_StartRuneFadeAnimation + 1] = markerIdx
  StartRuneFadeAnimation()
end
function StartRuneFadeAnimation(level, go)
  if CallbackQueue_StartRuneFadeAnimation == nil or #CallbackQueue_StartRuneFadeAnimation <= 0 then
    return
  end
  local markerIdx = table.remove(CallbackQueue_StartRuneFadeAnimation)
  local animObj = markerFX[markerIdx].fx
  local playbackRate = runeFadeAnimFrameLength / 30 / timeToFadeAway
  animObj:OnAnimationDone(thisObj, "RuneHasFaded", {Force = true})
  CallbackQueue_RuneHasFaded[#CallbackQueue_RuneHasFaded + 1] = markerIdx
  animObj:PlayAnimationToFrame(15, {Rate = playbackRate})
end
function RuneHasFaded(level, go)
  if CallbackQueue_RuneHasFaded == nil or #CallbackQueue_RuneHasFaded <= 0 then
    return
  end
  local markerIdx = table.remove(CallbackQueue_RuneHasFaded, 1)
  local marker = markerFX[markerIdx]
  marker.visible = false
  marker.fx:Hide()
  chiselTargets[marker.chiselTargetsIdx].active = true
  if IsTutorial then
    QueueBanter("stn_ChiselTutorialLostSuccess", {
      stn_ChiselTutorialLostRandomizer = math.random()
    })
  end
end
function RemoveValueFromCallbackQueue(queue, value)
  for i, v in ipairs(queue) do
    if v == value then
      table.remove(queue, i)
    end
  end
end
function SubmitRumble(effectName)
  currentRumble = effectName
  game.Blender.Start({Name = currentRumble})
end
function StopRumble(effectName)
  if effectName ~= nil then
    game.Blender.Stop({Name = effectName})
  end
end
function DisableInteraction()
  if interacting == true then
    interacting = false
    SetGamePadInput(false)
  end
end
function EnableInteraction()
  if interacting == false then
    interacting = true
    SetGamePadInput(true)
  end
end
function SetGamePadInput(inputActive)
  if inputActive == true then
    player.Pad:EnableGameButton(tweaks.ePad.kPadLeftStickX)
    player.Pad:EnableGameButton(tweaks.ePad.kPadLeftStickY)
  else
    player.Pad:DisableGameButton(tweaks.ePad.kPadLeftStickX)
    player.Pad:DisableGameButton(tweaks.ePad.kPadLeftStickY)
  end
end
function LuaHook_SpawnChisel()
  local chiselSpawnArgs = {
    AutoDelete = false,
    GameObject = player,
    Joint = "JOMeleeWeapon2"
  }
  chiselObject = game.FX.Spawn("chiselShard00", nil, chiselSpawnArgs)
  if chiselInteractOnStart then
    LD.ExecuteCallbacksForEvent(thisObj.Level, thisObj, chiselInteractOnStart)
  end
  PlaySoundOnInteractStart()
end
function LuaHook_CacheInput()
  if chiselObject == nil then
    return
  end
  gameX_cached = gameX
  gameY_cached = gameY
  StopRumble()
  DisableInteraction()
  local sqrDistance = (chiselTargets[indexOfClosestTarget].x - gameX_cached) * (chiselTargets[indexOfClosestTarget].x - gameX_cached) + (chiselTargets[indexOfClosestTarget].y - gameY_cached) * (chiselTargets[indexOfClosestTarget].y - gameY_cached)
  if sqrDistance <= thresholdOfSuccess then
    player:TriggerMoveEvent("LE_ChiselGameStabSuccess")
  else
    player:TriggerMoveEvent("LE_ChiselGameStabFail")
  end
  if IsTutorial then
    PauseBanter()
  end
end
function LuaHook_ChiselHit()
  if chiselObject == nil or runePuzzleSolved == true then
    return
  end
  TUT_LoadLibrary()
  TUT.HideTutorial()
  local sqrDistance = (chiselTargets[indexOfClosestTarget].x - gameX_cached) * (chiselTargets[indexOfClosestTarget].x - gameX_cached) + (chiselTargets[indexOfClosestTarget].y - gameY_cached) * (chiselTargets[indexOfClosestTarget].y - gameY_cached)
  if sqrDistance <= thresholdOfSuccess or game.GetMiniGameplaySkipped() == true then
    local newMarkerIdx = GetFirstAvailableMarkerIndex()
    gamePlanePoint = GetGamePlanePointClosestToChisel()
    markerFX[newMarkerIdx].fx:Show()
    if not IsBlackRuneCineActive() then
      markerFX[newMarkerIdx].fx:SetWorldPosition(gamePlanePoint)
    end
    markerFX[newMarkerIdx].fx:PlayAnimationToFrame(10, {Rate = 0.5})
    markerFX[newMarkerIdx].fx:OnAnimationDone(thisObj, "RuneIntroComplete", {Force = true})
    CallbackQueue_RuneIntroComplete[#CallbackQueue_RuneIntroComplete + 1] = newMarkerIdx
    markerFX[newMarkerIdx].visible = true
    markerFX[newMarkerIdx].chiselTargetsIdx = indexOfClosestTarget
    chiselTargets[indexOfClosestTarget].active = false
    for i = 1, #markerFX do
      if i ~= newMarkerIdx and markerFX[i].visible then
        RemoveValueFromCallbackQueue(CallbackQueue_StartRuneFadeAnimation, i)
        RemoveValueFromCallbackQueue(CallbackQueue_RuneHasFaded, i)
        markerFX[i].fx:ClearAllAnimationCallbacks()
        local framesToAnimate = markerFX[i].fx.AnimFrame - 10
        local playBackRate = framesToAnimate / 10
        markerFX[i].fx:OnAnimationDone(thisObj, "StartRuneFadeAnimation", {Force = true})
        CallbackQueue_StartRuneFadeAnimation[#CallbackQueue_StartRuneFadeAnimation + 1] = i
        markerFX[i].fx:PlayAnimationToFrame(10, {
          Rate = -playBackRate / 2
        })
      end
    end
    local hitEffect = game.FX.Spawn("p_chisel_hit_success", nil, {AutoDelete = true})
    hitEffect:SetWorldPosition(gamePlanePoint)
    PlaySoundOnChiselHit(1)
    RemoveSuccessIndicator()
    StopRumble("FFB_LOOP_LARGE")
    game.FX.SubmitEffect({
      EffectName = "FSE_shake_temp_Generic_Large",
      Duration = 1
    })
  else
    local missEffect = game.FX.Spawn("p_chisel_hit_fail", nil, {AutoDelete = true})
    missEffect:SetWorldPosition(gamePlanePoint)
    PlaySoundOnChiselHit(0)
    RemoveSuccessIndicator()
    StopRumble("FFB_LOOP_SMALL")
    game.FX.SubmitEffect({
      EffectName = "FSE_shake_temp_Generic_Small",
      Duration = 1
    })
  end
end
function LuaHook_ChiselHit_AnimOver()
  EnableInteraction()
  game.SubObject.Wake(thisObj)
  if IsTutorial then
    UnpauseBanter()
  end
end
function LuaHook_SoundBeginMinigame()
  SetSoundOnMinigameBegin()
end
function LuaHook_SoundEndMinigame()
  SetSoundOnMinigameEnd()
end
function LuaHook_HideTutorial()
  TUT_LoadLibrary()
  TUT.HideTutorial()
  StopRumble("FFB_LOOP_SMALL")
  StopRumble("FFB_LOOP_LARGE")
end
function LuaHook_SpawnBloodRune()
  bloodRune:Show()
  bloodRune:JumpAnimationToFrame(0)
  bloodRune:PlayAnimationToEnd()
end
function LuaHook_StartBaldurCine()
  if IsBlackRuneCineActive() then
    uiCalls.UI_Global_Menu_Clear_Exclusive_Access("GlobalInWorldMenu")
    uiCalls.UI_Journal_Log_Unpause()
    player:AbortInteract()
    LD.CallFunctionAfterDelay(function()
      for _, f in pairs(markerFX) do
        f.fx:Hide()
      end
    end, 10)
    LD.CallFunctionAfterDelay(LuaHook_KillChisel, 1)
    local peak800 = game.FindLevel("Peak800_DragonRide")
    peak800:GetGameObject("Cine_BaldurReturns"):CallScript("SetupBaldurReturnsSequence")
    player.Pad:ResetLightColor()
  end
end
function IntializeTutorialBanter()
  IsTutorial = true
  BanterIsAvailable = true
  BanterQueue = {}
end
function QueueBanter(banterLine, factTable)
  for _, v in pairs(BanterQueue) do
    if v.Line == banterLine then
      return
    end
  end
  table.insert(BanterQueue, {Line = banterLine, Facts = factTable})
  PlayNextBanter()
end
function TutorialBanterFinished()
  LD.CallFunctionAfterDelay(function()
    BanterIsAvailable = true
  end, GlobalBanterCooldown)
end
function PlayNextBanter()
  if #BanterQueue <= 0 then
    return
  end
  if not BanterIsAvailable then
    return
  end
  local nextBanter = table.remove(BanterQueue, 1)
  local banterLine = nextBanter.Line
  local factTable = nextBanter.Facts
  if factTable ~= nil then
    for name, value in pairs(factTable) do
      game.Audio.SetBanterFact(name, value)
    end
  end
  BanterIsAvailable = false
  game.Audio.PlayBanter(banterLine, function()
    LD.CallFunctionAfterDelay(TutorialBanterFinished, GlobalBanterCooldown)
  end)
end
function PauseBanter()
  if TroubleFindingSpotTimer and TroubleFindingSpotTimer.running then
    TroubleFindingSpotTimer:Pause()
  end
  if HoverOverSuccessTimer and HoverOverSuccessTimer.running then
    HoverOverSuccessTimer:Pause()
  end
  BanterIsAvailable = false
end
function UnpauseBanter()
  if TroubleFindingSpotTimer and not TroubleFindingSpotTimer.running then
    TroubleFindingSpotTimer:Unpause()
  end
  if HoverOverSuccessTimer and not HoverOverSuccessTimer.running then
    HoverOverSuccessTimer:Unpause()
  end
  BanterIsAvailable = true
  PlayNextBanter()
end
function TriggerTutorialTroubleFindingTimer()
  if TroubleFindingSpotTimer == nil then
    timers_LoadLibrary()
    TroubleFindingSpotTimer = timers.StartLevelTimer(TroubleFindingSpotTime, TroubleFindingSpotTimerCallback)
  end
  if TroubleFindingSpotTimer:GetElapsedTime() > 0 or TroubleFindingSpotTimer:GetElapsedTime() < 0 then
    return
  end
  TroubleFindingSpotTimer:Restart()
end
function RestartTutorialTroubleFindingTimer()
  if TroubleFindingSpotTimer == nil then
    timers_LoadLibrary()
    TroubleFindingSpotTimer = timers.StartLevelTimer(TroubleFindingSpotTime, TroubleFindingSpotTimerCallback)
  else
    TroubleFindingSpotTimer:Restart()
  end
end
function StopTutorialTroubleFindingTimer()
  if TroubleFindingSpotTimer ~= nil then
    TroubleFindingSpotTimer:Reset()
  end
end
function TriggerHoverOverSuccessTimer()
  if HoverOverSuccessOnCooldown then
    return
  end
  if HoverOverSuccessTimer == nil then
    timers_LoadLibrary()
    HoverOverSuccessTimer = timers.StartLevelTimer(HoverOverSuccessTime, HoverOverSuccessTimerCallback)
  end
  if HoverOverSuccessTimer:GetElapsedTime() > 0 or HoverOverSuccessTimer:GetElapsedTime() < 0 then
    return
  end
  HoverOverSuccessTimer:Restart()
end
function RestartHoverOverSuccessTimer()
  if HoverOverSuccessOnCooldown then
    return
  end
  if HoverOverSuccessTimer == nil then
    timers_LoadLibrary()
    HoverOverSuccessTimer = timers.StartLevelTimer(HoverOverSuccessTime, HoverOverSuccessTimerCallback)
  else
    HoverOverSuccessTimer:Restart()
  end
end
function StopHoverOverSuccessTimer()
  if HoverOverSuccessTimer ~= nil then
    HoverOverSuccessTimer:Reset()
  end
end
function HoverOverSuccessTimerCallback()
  QueueBanter("stn_ChiselTutorialHit", {
    stn_ChiselTutorialHitRandomizer = math.random()
  })
  HoverOverSuccessOnCooldown = true
  LD.CallFunctionAfterDelay(function()
    HoverOverSuccessOnCooldown = false
    RestartHoverOverSuccessTimer()
  end, HoverOverSuccessCooldown)
end
function TroubleFindingSpotTimerCallback()
  QueueBanter("stn_ChiselTutorialTrouble")
  RestartTutorialTroubleFindingTimer()
end
function EnableCinematicTrigger()
  triggerCinematic = true
end
function DisableCinematicTrigger()
  triggerCinematic = false
end
function IsBlackRuneCineActive()
  return difficulty == "BlackRuneCine" and LD.GetEntityVariable("CompletedCineNumber") > 400
end
function InteroplateColor(A, B, Alpha)
  local A_str = string.format("%x", A)
  local B_str = string.format("%x", B)
  local A_r = tonumber(string.sub(A_str, 1, 1) .. string.sub(A_str, 2, 2), 16) / 255
  local A_g = tonumber(string.sub(A_str, 3, 3) .. string.sub(A_str, 4, 4), 16) / 255
  local A_b = tonumber(string.sub(A_str, 5, 5) .. string.sub(A_str, 6, 6), 16) / 255
  local B_r = tonumber(string.sub(B_str, 1, 1) .. string.sub(A_str, 2, 2), 16) / 255
  local B_g = tonumber(string.sub(B_str, 3, 3) .. string.sub(A_str, 4, 4), 16) / 255
  local B_b = tonumber(string.sub(B_str, 5, 5) .. string.sub(A_str, 6, 6), 16) / 255
  local alpha_r = LerpValue(A_r, B_r, Alpha)
  local alpha_g = LerpValue(A_g, B_g, Alpha)
  local alpha_b = LerpValue(A_b, B_b, Alpha)
  local result_r = math.floor(alpha_r * 255)
  local result_g = math.floor(alpha_g * 255)
  local result_b = math.floor(alpha_b * 255)
  local result_str = string.format("%x", result_r)
  if result_r <= 15 then
    result_str = result_str .. "0"
  end
  result_str = result_str .. string.format("%x", result_g)
  if result_g <= 15 then
    result_str = result_str .. "0"
  end
  result_str = result_str .. string.format("%x", result_b)
  if result_b <= 15 then
    result_str = result_str .. "0"
  end
  return tonumber(result_str, 16)
end
function LerpValue(A, B, Alpha)
  return (B - A) * Alpha + A
end
function RandomFloat(min, max)
  return math.random() * (max - min) + min
end
function GetRandomPointInCircle(radiusMin, radiusMax)
  local angle = math.random() * 2 * math.pi
  return math.cos(angle) * RandomFloat(radiusMin, radiusMax), math.sin(angle) * RandomFloat(radiusMin, radiusMax)
end
function GetRandomPointInSquare(radiusMin, radiusMax)
  local x, y = GetRandomPointInCircle(radiusMin, radiusMax)
  local storageVector = engine.Vector.New(x, 0, y)
  storageVector = storageVector:RotateXZ(45)
  storageVector.x, storageVector.z = MapCoordinatesOnCircleToSquare(storageVector.x, storageVector.z)
  storageVector = storageVector:RotateXZ(-45)
  return storageVector.x, storageVector.z
end
function MapCoordinatesOnCircleToSquare(x, y)
  local radHalf = math.sqrt(0.5)
  local xCoef, yCoef
  if x * x >= y * y then
    xCoef = sgn(x)
    yCoef = sgn(x) * y / x
  else
    xCoef = sgn(y) * x / y
    yCoef = sgn(y)
  end
  local radical = math.sqrt(x * x + y * y)
  return xCoef * radical * radHalf, yCoef * radical * radHalf
end
function GetGamePlanePointClosestToChisel()
  if not chiselObject or not chiselObject.Spawned then
    return nil
  end
  local ji = thisObj:GetJointIndex("gameJoint")
  local orig = thisObj:GetWorldJointPosition(ji)
  local normal = thisObj:GetWorldForward(ji)
  local linkP = chiselObject:GetWorldJointPosition(chiselObject:GetJointIndex("linkJoint"))
  local coef_X = normal.x
  local coef_Y = normal.y
  local coef_Z = normal.z
  local D = normal.x * -orig.x + normal.y * -orig.y + normal.z * -orig.z
  D = -D
  local t = D - coef_X * linkP.x - coef_Y * linkP.y - coef_Z * linkP.z / (coef_X * coef_X + coef_Y * coef_Y + coef_Z * coef_Z)
  local result = engine.Vector.New(linkP.x + normal.x * t, linkP.y + normal.y, linkP.z + normal.z * t)
  return result
end
function sgn(x)
  return math.abs(x) / x
end
function GetFirstAvailableMarkerIndex()
  if IsBlackRuneCineActive() then
    local minDistance = 10000
    local indexOfMinMarker = -1
    for index, FX in ipairs(markerFX) do
      local distance = chiselObject:GetWorldJointPosition(chiselObject:GetJointIndex("linkJoint")):Distance(FX.fx:GetWorldPosition())
      if minDistance > distance then
        minDistance = distance
        indexOfMinMarker = index
      end
    end
    return indexOfMinMarker
  else
    for i = 1, #markerFX do
      if markerFX[i].visible == false then
        return i
      end
    end
  end
  return -1
end
local soundChiselBlockerEmitter, soundChiselEmitter
local soundEvents = {
  OnChiselHitTarget = "SND_DOOR_Chisel_Room_Chisel_Spike_Hit_Target",
  OnChiselHitMiss = "SND_DOOR_Chisel_Room_Chisel_Spike_Hit_Miss",
  OnDoorCollapse = "SND_DOOR_Chisel_Room_Door_Collapse",
  OnDoorExplode = "SND_DOOR_Chisel_Door_EXPL",
  OnInteractLP = "SND_DOOR_Chisel_Room_Chisel_Spike_Vibrate_LP"
}
local soundTargetProximityRTPCName = "DOOR_Chisel_Room_Target_Proximity"
local soundTargetProximityRTPCMinHearableValue = 0.5
local soundTargetProximityRTPCMaxValue = 0.99
local soundAllowRTPCUpdate = false
local soundEmitterNames = {
  ChiselBlockerEmitterName = "SNDChiselBlocker",
  ChiselEmitterName = "SNDLeftHand"
}
function SoundOnInit()
  soundChiselBlockerEmitter = thisObj:FindSingleSoundEmitterByName(soundEmitterNames.ChiselBlockerEmitterName)
  soundChiselEmitter = player:FindSingleSoundEmitterByName(soundEmitterNames.ChiselEmitterName)
end
function PlaySoundOnChiselHit(hitSuccess)
  if hitSuccess == 1 then
    LD.PlaySound(soundChiselEmitter, soundEvents.OnChiselHitTarget)
    SetSoundProximityRTPC(0)
  else
    LD.PlaySound(soundChiselEmitter, soundEvents.OnChiselHitMiss)
  end
end
function PlaySoundOnInteractStart()
  SetSoundProximityRTPC(0)
  LD.PlaySound(soundChiselEmitter, soundEvents.OnInteractLP, true)
end
function StopSoundOnInteractEnd()
  SetSoundProximityRTPC(0)
  LD.StopSound(soundChiselEmitter, soundEvents.OnInteractLP)
end
function PlaySoundOnDoorCollapse()
  LD.PlaySound(soundChiselBlockerEmitter, soundEvents.OnDoorCollapse)
  LD.PlaySoundAfterDelay(soundChiselBlockerEmitter, soundEvents.OnDoorExplode, 1.65)
end
function UpdateSoundProximityRTPC(percentProximity)
  if soundAllowRTPCUpdate then
    SetSoundProximityRTPC(percentProximity)
  end
end
function SetSoundProximityRTPC(percentProximity)
  local newValue = soundTargetProximityRTPCMinHearableValue + (soundTargetProximityRTPCMaxValue - soundTargetProximityRTPCMinHearableValue) * percentProximity
  game.Audio.SetBusLevelRTPCValue(soundTargetProximityRTPCName, newValue)
end
function SetSoundAllowRTPCUpdate(allowRTPCUpdate)
  soundAllowRTPCUpdate = allowRTPCUpdate
end
function SetSoundOnMinigameBegin()
  SetSoundAllowRTPCUpdate(true)
end
function SetSoundOnMinigameEnd()
  SetSoundAllowRTPCUpdate(false)
end
