--[[
Sprint mod for Minetest by GunshipPenguin

To the extent possible under law, the author(s)
have dedicated all copyright and related and neighboring rights
to this software to the public domain worldwide. This software is
distributed without any warranty.
]]

local math = math
local vector = vector

local pairs = pairs

local get_node = minetest.get_node
local get_gametime = minetest.get_gametime
local add_particlespawner = minetest.add_particlespawner
local get_player_by_name = minetest.get_player_by_name

local registered_nodes = minetest.registered_nodes

local get_hunger = mcl_hunger.get_hunger
local exhaust =  mcl_hunger.exhaust


--Configuration variables, these are all explained in README.md
mcl_sprint = {}

mcl_sprint.SPEED = 1.3

local players = {}

-- Returns true if the player with the given name is sprinting, false if not.
-- Returns nil if player does not exist.
function mcl_sprint.is_sprinting(playername)
	if players[playername] then
		return players[playername].sprinting
	else
		return nil
	end
end

minetest.register_on_joinplayer(function(player)
	local playerName = player:get_player_name()

	players[playerName] = {
		sprinting = false,
		timeOut = 0,
		shouldSprint = false,
		clientSprint = false,
		lastPos = player:get_pos(),
		sprintDistance = 0,
		fov = 1.0,
		channel = minetest.mod_channel_join("mcl_sprint:" .. playerName),
	}
end)
minetest.register_on_leaveplayer(function(player)
	local playerName = player:get_player_name()
	players[playerName] = nil
end)

local function cancelClientSprinting(name)
	players[name].channel:send_all("")
	players[name].clientSprint = false
end

local function setSprinting(playerName, sprinting) --Sets the state of a player (0=stopped/moving, 1=sprinting)
	local player = minetest.get_player_by_name(playerName)
	local controls = player:get_player_control()
	if not players[playerName] then
		return false
	end
	local player_data = players[playerName]
	local fov, fov_new, fade_time = player_data.fov
	local wielded_item_name = player:get_wielded_item():get_name()
	player_data.sprinting = sprinting
	if sprinting == true or controls.RMB and string.find(wielded_item_name, "mcl_bows:bow") and wielded_item_name ~= "mcl_bows:bow" then
		if sprinting == true then
			fov_new, fade_time = math.min(fov + 0.05, 1.2), .15
			playerphysics.add_physics_factor(player, "speed", "mcl_sprint:sprint", mcl_sprint.SPEED)
		else
			fov_new, fade_time =                        .7, .3
		end
	elseif sprinting == false and wielded_item_name ~= "mcl_bows:bow_0" and wielded_item_name ~= "mcl_bows:bow_1" and wielded_item_name ~= "mcl_bows:bow_2" then
		fov_new = math.max(fov - 0.05, 1.0)
		fade_time = 0.15
		if sprinting == false then
			playerphysics.remove_physics_factor(player, "speed", "mcl_sprint:sprint")
		end
	end
	if fov_new and fov_new ~= fov then
		player:set_fov(fov_new, true, fade_time)
		player_data.fov = fov_new
	end
	return true
end

-- Given the param2 and paramtype2 of a node, returns the tile that is facing upwards
local function get_top_node_tile(param2, paramtype2)
	if paramtype2 == "colorwallmounted" then
		paramtype2 = "wallmounted"
		param2 = param2 % 8
	elseif paramtype2 == "colorfacedir" then
		paramtype2 = "facedir"
		param2 = param2 % 32
	end
	if paramtype2 == "wallmounted" then
		if param2 == 0 then
			return 2
		elseif param2 == 1 then
			return 1
		else
			return 5
		end
	elseif paramtype2 == "facedir" then
		if param2 >= 0 and param2 <= 3 then
			return 1
		elseif param2 == 4 or param2 == 10 or param2 == 13 or param2 == 19 then
			return 6
		elseif param2 == 5 or param2 == 11 or param2 == 14 or param2 == 16 then
			return 3
		elseif param2 == 6 or param2 == 8 or param2 == 15 or param2 == 17 then
			return 5
		elseif param2 == 7 or param2 == 9 or param2 == 12 or param2 == 18 then
			return 4
		elseif param2 >= 20 and param2 <= 23 then
			return 2
		else
			return 1
		end
	else
		return 1
	end
end

minetest.register_on_modchannel_message(function(channel_name, sender, message)
	if channel_name == "mcl_sprint:" .. sender then
		players[sender].clientSprint = minetest.is_yes(message)
	end
end)

minetest.register_on_respawnplayer(function(player)
	cancelClientSprinting(player:get_player_name())
end)

minetest.register_globalstep(function(dtime)
	--Get the gametime
	local gameTime = get_gametime()

	--Loop through all connected players
	for playerName, playerInfo in pairs(players) do
		local player = get_player_by_name(playerName)
		if player then
			local ctrl = player:get_player_control()
			--Check if the player should be sprinting
			if players[playerName]["clientSprint"] or ctrl.aux1 and ctrl.up and not ctrl.sneak then
				players[playerName]["shouldSprint"] = true
			else
				players[playerName]["shouldSprint"] = false
			end

			local playerPos = player:get_pos()
			--If the player is sprinting, create particles behind and cause exhaustion
			if playerInfo["sprinting"] == true and not player:get_attach() and gameTime % 0.1 == 0 then
				-- Exhaust player for sprinting
				local lastPos = players[playerName].lastPos
				local dist = vector.distance({x=lastPos.x, y=0, z=lastPos.z}, {x=playerPos.x, y=0, z=playerPos.z})
				players[playerName].sprintDistance = players[playerName].sprintDistance + dist
				if players[playerName].sprintDistance >= 1 then
					local superficial = math.floor(players[playerName].sprintDistance)
					exhaust(playerName, mcl_hunger.EXHAUST_SPRINT * superficial)
					players[playerName].sprintDistance = players[playerName].sprintDistance - superficial
				end

				-- Sprint node particles
				local playerNode = get_node({x=playerPos["x"], y=playerPos["y"]-1, z=playerPos["z"]})
				local def = registered_nodes[playerNode.name]
				if def and def.walkable then
					add_particlespawner({
						amount = math.random(1, 2),
						time = 1,
						minpos = {x=-0.5, y=0.1, z=-0.5},
						maxpos = {x=0.5, y=0.1, z=0.5},
						minvel = {x=0, y=5, z=0},
						maxvel = {x=0, y=5, z=0},
						minacc = {x=0, y=-13, z=0},
						maxacc = {x=0, y=-13, z=0},
						minexptime = 0.1,
						maxexptime = 1,
						minsize = 0.5,
						maxsize = 1.5,
						collisiondetection = true,
						attached = player,
						vertical = false,
						node = playerNode,
						node_tile = get_top_node_tile(playerNode.param2, def.paramtype2),
					})
				end
			end

			--Adjust player states
			players[playerName].lastPos = playerPos
			if players[playerName]["shouldSprint"] == true then --Stopped
				local sprinting
				-- Prevent sprinting if hungry or sleeping
				if (mcl_hunger.active and get_hunger(player) <= 6)
				or (player:get_meta():get_string("mcl_beds:sleeping") == "true") then
					sprinting = false
					cancelClientSprinting(playerName)
				else
					sprinting = true
				end
				setSprinting(playerName, sprinting)
			elseif players[playerName]["shouldSprint"] == false then
				setSprinting(playerName, false)
			end

		end
	end
end)
