--                           TOGEN'S DR. ROBOTNIK'S
--  _____ _                 _              ___        _                 _     
-- /  ___(_)               | |            / _ \      (_)               | |    
-- \ `--. _ _ __ ___  _ __ | | ___ _ __  / /_\ \_ __  _ _ __ ___   __ _| |    
--  `--. \ | '_ ` _ \| '_ \| |/ _ \ '__| |  _  | '_ \| | '_ ` _ \ / _` | |    
-- /\__/ / | | | | | | |_) | |  __/ |    | | | | | | | | | | | | | (_| | |____
-- \____/|_|_| |_| |_| .__/|_|\___|_|    \_| |_/_| |_|_|_| |_| |_|\__,_\_____/
--                   | |                                                      
--                   |_|            V1.00.01

local CURRENT_VERSION = 010001
local ANIMATION_DEFINITIONS = {}
local ANIMATION_HELPERS = {}
local SEQUENCE_HOOKS = {}

-- This script is an Open Asset by Togen. As part of its functions, it allows newer versions of the script to overwrite its functions.
-- If you think somebody's got a weird hack that is doing bad things to other Simpler AnimaLs, either contact me or make use of the SRB2 MB's Porting rules.

--  _   _  _____ _     ______ ___________  ______ _   _ _   _ _____ _____ _____ _____ _   _  _____ 
-- | | | ||  ___| |    | ___ \  ___| ___ \ |  ___| | | | \ | /  __ \_   _|_   _|  _  | \ | |/  ___|
-- | |_| || |__ | |    | |_/ / |__ | |_/ / | |_  | | | |  \| | /  \/ | |   | | | | | |  \| |\ `--. 
-- |  _  ||  __|| |    |  __/|  __||    /  |  _| | | | | . ` | |     | |   | | | | | | . ` | `--. \
-- | | | || |___| |____| |   | |___| |\ \  | |   | |_| | |\  | \__/\ | |  _| |_\ \_/ / |\  |/\__/ /
-- \_| |_/\____/\_____/\_|   \____/\_| \_| \_|    \___/\_| \_/\____/ \_/  \___/ \___/\_| \_/\____/ 

function ANIMATION_HELPERS.getPlayer(mobj)
  if mobj.player then -- the actual player
    return mobj.player
  elseif mobj.target.player then -- the target's player for overlays and stuff.
    return mobj.target.player
  elseif mobj.target.target.player then -- :weary:
    return mobj.target.target.player
  end
end

function ANIMATION_HELPERS.getSkin(mobj)
  if mobj.skin then -- the actual skin
    return mobj.skin
  elseif mobj.target.skin then -- the target's skin for secondcolor signpost overlays and stuff.
    return mobj.target.skin
  elseif mobj.target.target.skin then --  just in case.
    return mobj.target.target.skin
  end
end

function ANIMATION_HELPERS.isSpinningWheels(mobj)
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  local buttons = K_GetKartButtons(player)
  local onground = P_IsObjectOnGround(player.mo) -- the player object specifically, not signposts!
  return ((buttons & BT_ACCELERATE) or (onground and player.speed > 0))
end

function ANIMATION_HELPERS.isLookingBack(mobj)
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  local buttons = K_GetKartButtons(player)
  return (buttons & BT_LOOKBACK)
end

function ANIMATION_HELPERS.isTurningRight(mobj)
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  local minturn = KART_FULLTURN/8
  return (player.cmd.turning < (minturn * -1))
end

function ANIMATION_HELPERS.isTurningLeft(mobj)
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  local minturn = KART_FULLTURN/8
  return (player.cmd.turning > (minturn))
end

function ANIMATION_HELPERS.isDriftingRight(mobj)
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  if (abs(player.aizdriftturn) >= ANGLE_90) then
    return false
  elseif (player.curshield == KSHIELD_TOP) then
    return ANIMATION_HELPERS.isTurningLeft(mobj)
  else
    return (player.drift < 0)
  end
end

function ANIMATION_HELPERS.isDriftingLeft(mobj)
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  if (abs(player.aizdriftturn) >= ANGLE_90) then
    return false
  elseif (player.curshield == KSHIELD_TOP) then
    return ANIMATION_HELPERS.isTurningRight(mobj)
  else
    return (player.drift > 0)
  end
end

function ANIMATION_HELPERS.isFast(mobj)
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  local fastspeed = (K_GetKartSpeed(player, false, true) * 17) / 20
  local speedthreshold = player.mo.scale / 8 -- specifcally the player object and not a signpost!
  return ((player.speed >= fastspeed) and (player.speed >= (player.lastspeed - speedthreshold)))
end

function ANIMATION_HELPERS.isMovingForward(mobj) -- Checks if the player is moving forward (conveyors, etc. ignored.)
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  local mymovangle = R_PointToAngle2(0, 0, player.rmomx, player.rmomy)
  local anglediff = (AngleFixed(mymovangle - mobj.angle)/FRACUNIT) % 360
  return (player.speed > 0 and not (anglediff > 90 and anglediff < 270))
end

function ANIMATION_HELPERS.isMovingBackward(mobj) -- Checks if the player is moving backward (conveyors, etc. ignored.)
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  local mymovangle = R_PointToAngle2(0, 0, player.rmomx, player.rmomy)
  local anglediff = (AngleFixed(mymovangle - mobj.angle)/FRACUNIT) % 360
  return (player.speed > 0 and anglediff > 90 and anglediff < 270)
end

function ANIMATION_HELPERS.isAirborne(mobj) -- Checks if the player is airborne.
  return not P_IsObjectOnGround(mobj)
end

function ANIMATION_HELPERS.isAscending(mobj) -- Checks if the player is rising through the air.
  local flipped = mobj.eflags & MFE_VERTICALFLIP
  local fallspeed = mobj.momz
  if flipped then
    fallspeed = $ * -1
  end
  return fallspeed > 0 and not P_IsObjectOnGround(mobj)
end

function ANIMATION_HELPERS.isDescending(mobj) -- Checks if the player is falling through the air.
  local flipped = mobj.eflags & MFE_VERTICALFLIP
  local fallspeed = mobj.momz
  if flipped then
    fallspeed = $ * -1
  end
  return fallspeed < 0 and not P_IsObjectOnGround(mobj)
end

function ANIMATION_HELPERS.isTricking(mobj) -- Checks if the player has a trick boost ready.
  local tricking = ANIMATION_HELPERS.getPlayer(mobj).trickpanel
  local TRICKSTATE_READY = 1 -- pov your value is predictable but you are not exposed to Lua
  if tricking then
    return (tricking > TRICKSTATE_READY) -- people are already talking about porting acro/otrx so let's leave some space here...?
  end
  return false
end

function ANIMATION_HELPERS.SquaredSlow(mobj, step) -- Function to slow down animations at low speeds.
  local player = ANIMATION_HELPERS.getPlayer(mobj)
  
  local currentspeed = player.speed
  local threshold = (K_GetKartSpeed(player, false, true) * 17) / 20
  local quarterspeed = currentspeed < threshold / 4 and mobj.Simpler_AnimaL.total_timer %4 ~= 0
  local halfspeed = currentspeed < threshold / 2 and mobj.Simpler_AnimaL.total_timer %2 ~= 0

  if quarterspeed or halfspeed then
    mobj.Simpler_AnimaL.timer = $ + step
  end
end

--   ___   _   _ ________  ___  ___ _____ _____ _____ _   _ 
--  / _ \ | \ | |_   _|  \/  | / _ \_   _|_   _|  _  | \ | |
-- / /_\ \|  \| | | | | .  . |/ /_\ \| |   | | | | | |  \| |
-- |  _  || . ` | | | | |\/| ||  _  || |   | | | | | | . ` |
-- | | | || |\  |_| |_| |  | || | | || |  _| |_\ \_/ / |\  |
-- \_| |_/\_| \_/\___/\_|  |_/\_| |_/\_/  \___/ \___/\_| \_/
                                                         
local function this_Simpler_AnimaL_setup(...) -- first argument is character name, all others are sprite names.
  local simple_args = {...}
  local thischaracter = ""
  for k, v in ipairs(simple_args) do
    if k == 1 then
      _G["SIMPLER_ANIMAL_DEFINITIONS"][v] = {} -- track character name.
      thischaracter = v
    else
      freeslot("SPR2_" + v) -- freeslot sprite name.
    end
  end
end

local function this_Simpler_AnimaL_thinker(mobj)
  if not (mobj and mobj.valid) then
    return
  elseif mobj.type == MT_PLAYER and _G["SIMPLER_ANIMAL_DEFINITIONS"][mobj.skin] then
    if not mobj.Simpler_AnimaL then
	  mobj.Simpler_AnimaL = {["timer"] = 0, ["total_timer"] = 0}
	end
    local anime = mobj.Simpler_AnimaL
	anime.last_hook = nil
    
    local skin = ANIMATION_HELPERS.getSkin(mobj)
    local skin_data = skins[skin]
    local last_sequence = anime.last_sequence
      
    local sequence_number = mobj.sprite2
    local expected_sequence = spr2names[sequence_number] -- Current vanilla animation.
    local expected_start = 0
    local expected_loop = 0
    local expected_dur = 0
    
    if mobj.hitlag then -- Most people aren't going to want characters to animate during this.
	  if mobj.sprite2 ~= SPR2_DEAD
        mobj.sprite2 = spr2names[last_sequence]
        mobj.frame = anime.last_frame
		anime.last_hook = "simpler_animal_hitstun_interrupt" -- I don't know why you'd need this, but let's track it anyway.
	  else
	    anime.last_hook = "simpler_animal_death_interrupt"
	  end
      return
    end
    
    for key, hook in pairs(_G["SIMPLER_ANIMAL_HOOKS"]) do
      local result, start, loop, dur = hook(mobj) -- Check hooks for return values.
      if result ~= nil then
        expected_sequence = result -- Desired custom animation.
        expected_start = (start or A)
        expected_loop = (loop or A)
        expected_dur = (dur or A)
        sequence_number = spr2names[expected_sequence]
        anime.last_hook = key
        break
      end
    end
      
    anime.total_timer = $ + 1
    if (expected_sequence ~= last_sequence) then -- Start a new sequence.
      anime.timer = expected_start
      anime.total_timer = 0 -- This value only changes upon a state change, use it instead of leveltime for animation timing.
      anime.last_sequence = expected_sequence
    elseif (expected_start > expected_dur) and (expected_start > A) then-- Backing up.
      anime.timer = ($ - 1)
    elseif (expected_dur > A) then -- Advance forward.
      anime.timer = ($ + 1)
    else
      anime.timer = 0
      anime.total_timer = 0
    end

    local temp_timer = anime.timer % (1 + abs(expected_dur - expected_start)) -- Get progress between start and loop.
    if temp_timer < 0 then -- Fix an underflow.
      temp_timer = $ + max(expected_start, expected_dur)
    end
      
    if anime.last_hook then
	  mobj.sprite2 = sequence_number
      mobj.frame = min(expected_start, expected_dur) + temp_timer -- Add timer to lower value of end or start.
    else
      anime.total_timer = -1
    end
    anime.last_frame = mobj.frame
  end
end

--  _____ ________  _________  ___ _____ ___________ _____ _     _____ _______   __
-- /  __ \  _  |  \/  || ___ \/ _ \_   _|_   _| ___ \_   _| |   |_   _|_   _\ \ / /
-- | /  \/ | | | .  . || |_/ / /_\ \| |   | | | |_/ / | | | |     | |   | |  \ V / 
-- | |   | | | | |\/| ||  __/|  _  || |   | | | ___ \ | | | |     | |   | |   \ /  
-- | \__/\ \_/ / |  | || |   | | | || |  _| |_| |_/ /_| |_| |_____| |_  | |   | |  
--  \____/\___/\_|  |_/\_|   \_| |_/\_/  \___/\____/ \___/\_____/\___/  \_/   \_/  

local last_version = nil
if not (_G["SIMPLER_ANIMAL_VERSION"]) then
  -- Step 1: Check if this is the first Simpler AnimaL instance.
  rawset(_G, "SIMPLER_ANIMAL_VERSION", CURRENT_VERSION)
  rawset(_G, "SIMPLER_ANIMAL_ALL_VERSIONS", {CURRENT_VERSION})
  rawset(_G, "SIMPLER_ANIMAL_SETUP", this_Simpler_AnimaL_setup)
  rawset(_G, "SIMPLER_ANIMAL_THINKER", this_Simpler_AnimaL_thinker)
  
  rawset(_G, "SIMPLER_ANIMAL_DEFINITIONS", {}) -- dictionary full of character names
  rawset(_G, "SIMPLER_ANIMAL_HELPERS", {}) -- helper functions
  rawset(_G, "SIMPLER_ANIMAL_HOOKS", {}) -- creator-made hooks
    
elseif (_G["SIMPLER_ANIMAL_VERSION"] < CURRENT_VERSION) then
  last_version = _G["SIMPLER_ANIMAL_VERSION"]
  print("[Simpler AnimaL] Running an update.")
  
  -- Step 1: Check if we can update other instances.
  table.insert(SIMPLER_ANIMAL_ALL_VERSIONS, CURRENT_VERSION)
  rawset(_G, "SIMPLER_ANIMAL_VERSION", CURRENT_VERSION)
  rawset(_G, "SIMPLER_ANIMAL_SETUP", this_Simpler_AnimaL_setup)
  rawset(_G, "SIMPLER_ANIMAL_THINKER", this_Simpler_AnimaL_thinker)
end

if not SIMPLER_ANIMAL_HARDHOOKS then
  local this_player_version = 1
  local this_overlay_version = 1
  rawset(_G, "SIMPLER_ANIMAL_HARDHOOKS", {})
  
  if (not SIMPLER_ANIMAL_HARDHOOKS.player) then 
    addHook("PostThinkFrame", function(mo)
      if SIMPLER_ANIMAL_HARDHOOKS.player > this_player_version then return end
      for player in players.iterate do
        if not (player.valid and player.mo) then continue end
        SIMPLER_ANIMAL_THINKER(player.mo)
      end
    end)
      
    SIMPLER_ANIMAL_HARDHOOKS.player = this_player_version
  end
    
  if (not SIMPLER_ANIMAL_HARDHOOKS.overlay) then 
    addHook("MobjThinker", function(mo)
      if SIMPLER_ANIMAL_HARDHOOKS.overlay > this_overlay_version then return end
      return SIMPLER_ANIMAL_THINKER(mo)
    end, MT_OVERLAY)
    SIMPLER_ANIMAL_HARDHOOKS.overlay = this_overlay_version
  end
end

for key, helper in pairs(ANIMATION_HELPERS) do
  if not SIMPLER_ANIMAL_HELPERS[key] then
    SIMPLER_ANIMAL_HELPERS[key] = helper
  end
end

-- ______ ___________ _   _ _____ 
-- |  _  \  ___| ___ \ | | |  __ \
-- | | | | |__ | |_/ / | | | |  \/
-- | | | |  __|| ___ \ | | | | __ 
-- | |/ /| |___| |_/ / |_| | |_\ \
-- |___/ \____/\____/ \___/ \____/

local function this_Simpler_AnimaL_debug()
  print("Simpler AnimaL versions: " + table.concat(SIMPLER_ANIMAL_ALL_VERSIONS, ", "))
  local keys = {}
  
  for k, v in pairs(SIMPLER_ANIMAL_DEFINITIONS) do
    table.insert(keys, k)
  end
  
  print("Simpler AnimaL definitions: " + table.concat(keys, ", "))
end

if not SIMPLER_ANIMAL_DEBUG_FUNC then -- Add this console command for some testing purposes.
  rawset(_G, "SIMPLER_ANIMAL_DEBUG_FUNC", this_Simpler_AnimaL_debug)
  COM_AddCommand("simpler_animal", function()
    SIMPLER_ANIMAL_DEBUG_FUNC()
  end, COM_LOCAL)
end