------------------
-- Draconis API --
------------------
----- Ver 1.1 ----

local S = minetest.get_translator("c_dragons")

local l_time = 0
local l_N = 2048
local l_samples = {}
local l_ctr = 0
local l_sumsq = 0
local l_sum = 0
local l_max = 0.1

----------
-- Math --
----------

local pi = math.pi
local random = math.random
local abs = math.abs
local min = math.min
local max = math.max
local floor = math.floor
local ceil = math.ceil
local deg = math.deg
local atan2 = math.atan2
local sin = math.sin
local cos = math.cos
local function R(x) -- Round to nearest multiple of 0.5
	return x + 0.5 - (x + 0.5) % 1
end
local function diff(a, b) -- Get difference between 2 angles
    return atan2(sin(b - a), cos(b - a))
end

local vec_dir = vector.direction
local vec_dist = vector.distance
local vec_new = vector.new
local vec_sub = vector.subtract
local vec_add = vector.add

local dir2yaw = minetest.dir_to_yaw
local yaw2dir = minetest.yaw_to_dir

local function clamp(n)
    if n < -180 then
        n = n + 360
    elseif n > 180 then
        n = n - 360
    end
    if n < -60 then
        n = -60
    elseif n > 60 then
        n = 60
    end
    return n
end

local function interp(a, b, w)
    if abs(a - b) > deg(pi) then
        if a < b then
            return ((a + (b - a) * w) + (deg(pi) * 2))
        elseif a > b then
            return ((a + (b - a) * w) - (deg(pi) * 2))
        end
    end
    return a + (b - a) * w
end

----------------------
-- Helper Functions --
----------------------

local str_find = string.find

local hitbox = mob_core.get_hitbox

local function find_closest_pos(tbl, pos)
    local iter = 2
    if #tbl < 2 then return end
    local closest = tbl[1]
    while iter < #tbl do
        if vec_dist(pos, closest) < vec_dist(pos, tbl[iter + 1]) then
            iter = iter + 1
        else
            closest = tbl[iter]
            iter = iter + 1
        end
    end
    if iter >= #tbl and closest then return closest end
end

local function get_collision_in_radius(pos, width, height)
    local pos1 = vector.new(pos.x - width, pos.y, pos.z - width)
    local pos2 = vector.new(pos.x + width, pos.y + height, pos.z + width)
    local collisions = {}
    for z = pos1.z, pos2.z do
        for y = pos1.y, pos2.y do
            for x = pos1.x, pos2.x do
                local npos = vector.new(x, y, z)
                local name = minetest.get_node(npos).name
                if minetest.registered_nodes[name].walkable then
                    table.insert(collisions, npos)
                end
            end
        end
    end
    return collisions
end

local moveable = mob_core.is_moveable

function draconis.get_collision_avoidance_pos(self)
    local width = hitbox(self)[4]
    local pos = self.object:get_pos()
    local yaw = self.object:get_yaw()
    local outset = width * 2
    local ahead = vector.add(pos, vector.multiply(minetest.yaw_to_dir(yaw), outset))
    local can_fit = moveable(ahead, width, self.height)
    if not can_fit then
        local collisions = get_collision_in_radius(ahead, width, self.height)
        local obstacle = find_closest_pos(collisions, pos)
        if obstacle then
            local avoidance_path = vector.normalize((vector.subtract(pos, obstacle)))
            local avoidance_pos = vector.add(pos, vector.multiply(avoidance_path, outset))
            local magnitude = (width * 2) - vec_dist(pos, obstacle)
            return avoidance_pos, magnitude
        end
    end
end

function draconis.get_line_of_sight(a, b)
    local steps = floor(vec_dist(a, b))
    local line = {}

    for i = 0, steps do
        local pos

        if steps > 0 then
            pos = {
                x = a.x + (b.x - a.x) * (i / steps),
                y = a.y + (b.y - a.y) * (i / steps),
                z = a.z + (b.z - a.z) * (i / steps)
            }
        else
            pos = a
        end
        table.insert(line, pos)
    end

    if #line < 1 then
        return false
    else
        for i = 1, #line do
            local node = minetest.get_node(line[i])
            if minetest.registered_nodes[node.name].walkable
            and mobkit.get_node_height(line[i]) >= 4.5 then
                return false
            end
        end
    end
    return true
end

function draconis.get_collision(self, dir, range)
    local pos = self.object:get_pos()
    local pos2 = vector.add(pos, vector.multiply(dir, range or 16))
    local ray = minetest.raycast(pos, pos2, false, false)
    for pointed_thing in ray do
        if pointed_thing.type == "node" then
            return true
        end
    end
    return false
end

function draconis.ray_collision_detect(self)
    for i = 1, 179, 30 do
        local yaw_a = self.object:get_yaw() + math.rad(i)
        local dir_a = minetest.yaw_to_dir(yaw_a)
        local collision_a = draconis.get_collision(self, dir_a, hitbox(self)[4] + 4)
        if collision_a then
            local yaw_b = self.object:get_yaw() + math.rad(-i)
            local dir_b = minetest.yaw_to_dir(yaw_b)
            local collision_b = draconis.get_collision(self, dir_b, hitbox(self)[4] + 4)
            if not collision_b then
                return yaw_b
            end
        else
            return yaw_a
        end
    end
end

------------------
-- Registration --
------------------

function draconis.register_dragon(type, def)
    minetest.register_entity("c_dragons:" .. type .. "_dragon", {
        -- Stats
        max_hp = def.hp,
        view_range = 64,
        reach = 14,
        damage = 20,
        knockback = 4,
        lung_capacity = 60,
        floor_avoidance_range = 32,
        -- Movement & Physics
        max_speed = 16,
        stepheight = 1.76,
        jump_height = 1.26,
        max_fall = 100,
        buoyancy = 1,
        springiness = 0,
        turn_rate = 4,
        -- Visual
	glow = 3,
        collisionbox = {-2.45, 0, -2.45, 2.45, 5, 2.45},
        visual_size = {x = 35, y = 35},
        visual = "mesh",
        mesh = "draconis_" .. type .. "_dragon.b3d",
        textures = {
            "draconis_" .. type .. "_dragon_body.png^draconis_" .. type .. "_dragon_head_detail.png"
        },
        animation = {
            stand = {range = {x = 1, y = 60}, speed = 15, frame_blend = 0.3, loop = true},
            stand_fire = {range = {x = 70, y = 130}, speed = 15, frame_blend = 0.3, loop = true},
            wing_flap = {range = {x = 140, y = 200}, speed = 15, frame_blend = 0.3, loop = false},
            walk = {range = {x = 210, y = 250}, speed = 35, frame_blend = 0.3, loop = true},
            walk_fire = {range = {x = 260, y = 300}, speed = 35, frame_blend = 0.3, loop = true},
            takeoff = {range = {x = 310, y = 330}, speed = 25, frame_blend = 0.3, loop = false},
            fly_idle = {range = {x = 340, y = 380}, speed = 25, frame_blend = 0.3, loop = true},
            fly_idle_fire = {range = {x = 390, y = 430}, speed = 25, frame_blend = 0.3, loop = true},
            fly = {range = {x = 440, y = 480}, speed = 25, frame_blend = 0.3, loop = true},
            fly_fire = {range = {x = 490, y = 530}, speed = 25, frame_blend = 0.3, loop = true},
            dive_bomb = {range = {x = 540, y = 580}, speed = 25, frame_blend = 0.3, loop = true},
            death = {range = {x = 670, y = 670}, speed = 1, frame_blend = 2, prty = 3, loop = true},
            shoulder_idle = {range = {x = 680, y = 740}, speed = 10, frame_blend = 0.6, loop = true}
        },
        dynamic_anim_data = {
            yaw_factor = 0.11,
            swing_factor = 0.33,
            pivot_h = 0.5,
            pivot_v = 0.75,
            tail = {
                { -- Segment 1
                    pos = {
                        x = 0,
                        y = 0,
                        z = 0
                    },
                    rot = {
                        x = 180,
                        y = 180,
                        z = 1
                    }
                },
                { -- Segment 2
                    pos = {
                        x = 0,
                        y = 0.7,
                        z = 0
                    },
                    rot = {
                        x = 0,
                        y = 0,
                        z = 1
                    }
                },
                { -- Segment 3
                    pos = {
                        x = 0,
                        y = 1,
                        z = 0
                    },
                    rot = {
                        x = 0,
                        y = 0,
                        z = 1
                    }
                },
                { -- Segment 4
                    pos = {
                        x = 0,
                        y = 1,
                        z = 0
                    },
                    rot = {
                        x = 0,
                        y = 0,
                        z = 1
                    }
                }
            },
            head = {
                { -- Segment 1
                    pitch_offset = 20,
                    bite_angle = -20,
                    pitch_factor = 0.22,
                    pos = {
                        x = 0,
                        y = 0.83,
                        z = 0.036
                    },
                    rot = {
                        x = 0,
                        y = 0,
                        z = 0
                    }
                },
                { -- Segment 2
                    pitch_offset = -5,
                    bite_angle = 10,
                    pitch_factor = 0.22,
                    pos = {
                        x = 0,
                        y = 0.45,
                        z = 0
                    },
                    rot = {
                        x = 0,
                        y = 0,
                        z = 0
                    }
                },
                { -- Segment 3
                    pitch_offset = -5,
                    bite_angle = 10,
                    pitch_factor = 0.22,
                    pos = {
                        x = 0,
                        y = 0.45,
                        z = 0
                    },
                    rot = {
                        x = 0,
                        y = 0,
                        z = 0
                    }
                },
                { -- Head
                    pitch_offset = -20,
                    bite_angle = 5,
                    pitch_factor = 0.44,
                    pos = {
                        x = 0,
                        y = 0.41,
                        z = 0
                    },
                    rot = {
                        x = 0,
                        y = 0,
                        z = 0
                    }
                }
            }
        },
        -- Sound
        sounds = {
            roar = {
                {
                    name = "draconis_dragon_teen_roar",
                    gain = 1,
                    distance = 512,
                    length = 2.5
                }
            },
            random = {
                {
                    name = "draconis_dragon_teen_random_1",
                    gain = 1,
                    distance = 512,
                    length = 1
                },
                {
                    name = "draconis_dragon_teen_random_2",
                    gain = 1,
                    distance = 512,
                    length = 1
                },
                {
                    name = "draconis_dragon_teen_roar",
                    gain = 1,
                    distance = 512,
                    length = 2.5
                }
            },
            random2 = {
                {
                    name = "draconis_dragon_adult_1",
                    gain = 1,
                    distance = 512,
                    length = 2
                },
                {
                    name = "draconis_dragon_adult_2",
                    gain = 1,
                    distance = 512,
                    length = 3.5
                },
                {
                    name = "draconis_dragon_adult_3",
                    gain = 1,
                    distance = 512,
                    length = 4
                }
            },
            hurt = {
                {
                    name = "draconis_dragon_hurt",
                    gain = 1,
                    distance = 512
                },
                {
                    name = "draconis_dragon_hurt",
                    gain = 1,
                    pitch = 0.5,
                    distance = 512
                },
                {
                    name = "draconis_dragon_hurt",
                    gain = 1,
                    pitch = 0.25,
                    distance = 512
                },
            },
            flap = {
                name = "draconis_flap",
                gain = 0.5,
                distance = 512
            }
        },
        -- Basic
        physical = true,
        collide_with_objects = false,
        static_save = true,
        defend_owner = true,
        push_on_collide = true,
        punch_cooldown = 0.25,
        follow = draconis.global_meat,
        timeout = 0,
        open_jaw = draconis.open_jaw,
        move_head = draconis.move_head,
        move_tail = draconis.move_tail,
        physics = draconis.physics,
        logic = def.logic,
        get_staticdata = mobkit.statfunc,
        on_activate = draconis.on_activate,
        on_step = draconis.on_step,
        on_deactivate = function(self)
            -- TODO: instead of deleting immediately, we should let them time out slowly.
            self.hp = 0
            self.despawned = true
            draconis.dragon = nil
            return
        end,
        on_rightclick = function(self, clicker)
            return
        end,
        on_punch = function(self, puncher, _, tool_capabilities, dir)
            return
--[[
            self.idle_timer = mobkit.remember(self, "idle_timer", 0)
            mob_core.on_punch_basic(self, puncher, tool_capabilities, dir)
            draconis.hq_aerial_attack(self, 7, puncher)
]]
        end
    })
end

---------------------
-- Visual Entities --
---------------------

local function set_eyes(self, ent)
    local eyes = minetest.add_entity(self.object:get_pos(), ent)
    if eyes then
        eyes:set_attach(self.object, "Head", {x = 0, y = -0.975, z = -2.5}, {x = 69, y = 0, z = 180})
        return eyes
    end
end

local function set_shield(self, ent)
    local shield = minetest.add_entity(self.object:get_pos(), ent)
    if shield then
        shield:set_attach(self.object, "Torso.1")
        return shield
    end
end

-----------------------
-- Dynamic Animation --
-----------------------

function draconis.head_tracking(self)
    local yaw = self.object:get_yaw()
    if self.hp <= 0 then
        self:move_head(yaw)
        return
    end
    local pos = mobkit.get_stand_pos(self)
    local v = vector.add(pos, vector.multiply(yaw2dir(yaw), 8 * self.growth_scale))
    local head_height = 6 * self.growth_scale
    if self._anim == "fly_idle"
    or self._anim == "fly_idle_fire" then
        head_height = 11 * self.growth_scale
    end
    pos.x = v.x
    pos.y = pos.y + head_height
    pos.z = v.z
    if not self.head_tracking then
        local objects = minetest.get_objects_inside_radius(pos, 16)
        for _, object in ipairs(objects) do
            if object:is_player() then
                local dir_2_plyr = vector.direction(pos, object:get_pos())
                local yaw_2_plyr = dir2yaw(dir_2_plyr)
                if abs(yaw - yaw_2_plyr) < 1
                or abs(yaw - yaw_2_plyr) > 5.3 then
                    self.head_tracking = object
                end
                break
            end
        end
        if self._anim == "stand" then
            self:move_head(yaw)
        else
            self:move_head(self._tyaw)
        end
    else
        if not mobkit.exists(self.head_tracking) then
            self.head_tracking = nil
            return
        end
        local ppos = self.head_tracking:get_pos()
        ppos.y = ppos.y + 1.4
        local dir = vector.direction(pos, ppos)
        local tyaw = minetest.dir_to_yaw(dir)
        if abs(yaw - tyaw) > 1
        and abs(yaw - tyaw) < 5.3 then
            self.head_tracking = nil
            dir.y = 0
            return
        end
        self:move_head(tyaw, dir.y)
    end
end

-----------------
-- On Activate --
-----------------


function draconis.on_activate(self, staticdata, dtime_s)
    mob_core.on_activate(self, staticdata, dtime_s)
    while not self.eyes do
        if self.name == "c_dragons:blue_dragon" then
            self.eyes = set_eyes(self, "c_dragons:ice_eyes")
        elseif self.name == "c_dragons:purple_dragon" then
            self.eyes = set_eyes(self, "c_dragons:fire_eyes")
        elseif self.name == "c_dragons:black_dragon" then
            self.eyes = set_eyes(self, "c_dragons:fire_eyes2")
        end
    end
    if not self.shield_ent and (self.name == "c_dragons:purple_dragon" or self.name == "c_dragons:black_dragon") then
        self.shield_ent = set_shield(self, "c_dragons:shield")
    end
    self.logic_state = mobkit.recall(self, "logic_state") or "landed"
    self.flight_timer = mobkit.recall(self, "flight_timer") or 1
    self.age = mobkit.recall(self, "age") or 100
    self.growth_scale = mobkit.recall(self, "growth_scale") or 1
    self.time_from_last_sound = 0
    self.order = mobkit.recall(self, "order") or "wander"
    self.fly_allowed = mobkit.recall(self, "fly_allowed") or false
    self.idle_timer = mobkit.recall(self, "idle_timer") or 0
    self.greet_timer = mobkit.recall(self, "greet_timer") or 0
    self.current_phase = mobkit.recall(self, "current_phase") or 0
    self.hit_debouncer = mobkit.recall(self, "hit_debouncer") or 0
    self.altar_pos_x = mobkit.recall(self, "altar_pos_x") or 0
    self.altar_pos_y = mobkit.recall(self, "altar_pos_y") or 0
    self.altar_pos_z = mobkit.recall(self, "altar_pos_z") or 0
    self.beam_charge = mobkit.recall(self, "beam_charge") or 0
    self.swoop_charge = mobkit.recall(self, "swoop_charge") or 0
    self.shielded = mobkit.recall(self, "shielded") or false
    self.target_blacklist = {}
    self.fall_distance = 0
    self.flap_sound_timer = 1.5
    self.flap_sound_played = false
    self.roar_anim_length = 0
    self.anim_frame = 0
    self.frame_offset = 0
    mob_core.set_scale(self, self.growth_scale)
    self.drops = nil
    if self.dynamic_anim_data then
        local data = self.dynamic_anim_data
        if data.tail then
            draconis.move_tail(self)
        end
    end
    self.dtime = 0.1
    self:move_head(self.object:get_yaw())
    draconis.dragon = self
end

-------------
-- On Step --
-------------

local function flash_red(self)
	minetest.after(0.0, function()
		self.object:set_texture_mod("^[colorize:#FF000040")
		core.after(0.2, function()
			if mobkit.is_alive(self) then
				self.object:set_texture_mod("")
			end
		end)
	end)
end

function draconis.physics(self)
	local vel=self.object:get_velocity()
		-- dumb friction
	if self.isonground and not self.isinliquid then
		self.object:set_velocity({x= vel.x> 0.2 and vel.x*0.4 or 0,
								y=vel.y,
								z=vel.z > 0.2 and vel.z*0.4 or 0})
	end

	local surface = nil
	local surfnodename = nil
	local spos = mobkit.get_stand_pos(self)
	spos.y = spos.y+0.01
	local snodepos = mobkit.get_node_pos(spos)
	local surfnode = mobkit.nodeatpos(spos)
	while surfnode and surfnode.drawtype == 'liquid' do
		surfnodename = surfnode.name
		surface = snodepos.y+0.5
		if surface > spos.y+self.height then break end
		snodepos.y = snodepos.y+1
		surfnode = mobkit.nodeatpos(snodepos)
	end
	self.isinliquid = surfnodename
	if surface then
		local submergence = min(surface-spos.y,self.height)/self.height
		local buoyacc = 9.8*(self.buoyancy-submergence)
		mobkit.set_acceleration(self.object,
			{x=-vel.x*self.water_drag,y=buoyacc-vel.y*abs(vel.y)*0.4,z=-vel.z*self.water_drag})
	else
		self.object:set_acceleration({x=0,y=-9.8,z=0})
	end
end

function draconis.flap_sound(self)
    if not self._anim then return end
    if self._anim:match("fly") then
        if self.frame_offset > 30
        and not self.flap_sound_played then
            minetest.sound_play("draconis_flap", {
                object = self.object,
                gain = 1.0,
                max_hear_distance = 128,
                loop = false,
            })
            self.flap_sound_played = true
        elseif self.frame_offset < 10 then
            self.flap_sound_played = false
        end
    end
end

function draconis.set_adult_textures(self)
    local texture = self.object:get_properties().textures[1]
    local adult_overlay = "draconis_purple_dragon_head_detail.png"
    if self.name == "c_dragons:blue_dragon" then
        adult_overlay = "draconis_blue_dragon_head_detail.png"
    elseif self.name == "c_dragons:black_dragon" then
        adult_overlay = "draconis_black_dragon_head_detail.png"
    end
    self.object:set_properties({
        textures = {texture .. "^" .. adult_overlay}
    })
end

function draconis.on_step(self, dtime, moveresult)
    if self._anim then
        local aparms = self.animation[self._anim]
        if self.anim_frame ~= -1 then
            self.anim_frame = self.anim_frame + dtime
            self.frame_offset = floor(self.anim_frame * aparms.speed)
            if self.frame_offset > aparms.range.y - aparms.range.x then
                self.anim_frame = 0
                self.frame_offset = 0
            end
        end
    end
    self.turn_rate = 6 - (self.growth_scale * 1.5)
    mob_core.on_step(self, dtime, moveresult)
    if not mobkit.is_alive(self) then return end
    local pos = self.object:get_pos()
    if not self.eyes:get_yaw() then
        if self.name == "c_dragons:blue_dragon" then
            self.eyes = set_eyes(self, "c_dragons:ice_eyes")
        elseif self.name == "c_dragons:purple_dragon" then
            self.eyes = set_eyes(self, "c_dragons:fire_eyes")
        elseif self.name == "c_dragons:black_dragon" then
            self.eyes = set_eyes(self, "c_dragons:fire_eyes2")
        end
    end
    if not self.shield_ent and (self.name == "c_dragons:purple_dragon" or self.name == "c_dragons:black_dragon") then
        self.shield_ent = set_shield(self, "c_dragons:shield")
    end
    if mobkit.timer(self, 1) then
        self.time_from_last_sound = self.time_from_last_sound + 1
    end
    if mobkit.timer(self, 5) then
        if #self.target_blacklist > 0 then
            table.remove(self.target_blacklist, 1)
        end
    end
    if self.isonground or self.isinliquid then
        self.max_speed = 12
    else
        self.max_speed = 24
    end
    draconis.flap_sound(self)
    if self.isonground 
    and (self.object:get_rotation().x ~= 0
    or self.object:get_rotation().z ~= 0) then
        self.object:set_yaw(self.object:get_yaw())
    end
    draconis.head_tracking(self)
    self:open_jaw()
    if self.dynamic_anim_data then
        local data = self.dynamic_anim_data
        if data.tail then
            draconis.move_tail(self)
        end
    end
end

-----------------------------
-- Tamed Dragon Management --
-----------------------------

local function set_order(self, player, order)
    if order == "stand" then
        if self.isinliquid then return end
        mobkit.clear_queue_high(self)
        mobkit.clear_queue_low(self)
        self.object:set_velocity({x = 0, y = 0, z = 0})
        self.object:set_acceleration({x = 0, y = 0, z = 0})
        self.status = "stand"
        self.order = "stand"
        draconis.animate(self, "stand")
    end
    if order == "wander" then
        mobkit.clear_queue_high(self)
        mobkit.clear_queue_low(self)
        self.status = ""
        self.order = "wander"
    end
    if order == "follow" then
        mobkit.clear_queue_low(self)
        self.status = "following"
        self.order = "follow"
        draconis.hq_follow(self, 5, player)
    end
    mobkit.remember(self, "status", self.status)
    mobkit.remember(self, "order", self.order)
end

local mob_obj = {}

--------------
-- Spawning --
--------------

local function spawn_dragon(pos, mob)
    local age = 50
    if not pos then return false end
    local dragon = minetest.add_entity({x = pos.x, y = 100, z = pos.z}, mob)
    if dragon then
        local ent = dragon:get_luaentity()
	draconis.dragon = ent
        ent.altar_pos_x = mobkit.remember(ent, "altar_pos_x", pos.x)
        ent.altar_pos_y = mobkit.remember(ent, "altar_pos_y", pos.y)
        ent.altar_pos_z = mobkit.remember(ent, "altar_pos_z", pos.z)
        ent._mem = mobkit.remember(ent, "_mem", true)
        ent.age = mobkit.remember(ent, "age", age)
        ent.growth_scale = mobkit.remember(ent, "growth_scale", age * 0.01)
        if age <= 50 then
            ent.growth_stage = mobkit.remember(ent, "growth_stage", 2)
        end
        if age <= 75 then
            ent.growth_stage = mobkit.remember(ent, "growth_stage", 3)
        end
        if age > 75 then
            ent.growth_stage = mobkit.remember(ent, "growth_stage", 4)
        end
        mob_core.set_scale(ent, ent.growth_scale)
        mob_core.set_textures(ent)
        ent.drops = nil
    end
end

function draconis.spawn_dragon(pos, mob, mapgen, age)
    minetest.forceload_block(pos, false)
    minetest.after(4, function()
        spawn_dragon(pos, mob, mapgen, age)
        minetest.after(0.01, function()
            local loop = true
            local objects = minetest.get_objects_inside_radius({x=pos.x, y = 100, z = pos.z}, 0.5)
            for i = 1, #objects do
                local object = objects[i]
                if object
                and object:get_luaentity()
                and object:get_luaentity().name == mob then
                    loop = false
                end
            end
            minetest.after(1, function()
                minetest.forceload_free_block(pos)
            end)
            if loop then
                draconis.spawn_dragon(pos, mob, mapgen, age)
            end 
        end)
    end)
end


-----------------
-- Pathfinding --
-----------------

function draconis.adjust_pos(self, pos2)
    local width = hitbox(self)[4] + 2
    local can_fit = moveable(pos2, width, self.height)
    if not can_fit then
        local minp = vector.new(pos2.x - width, pos2.y - 1, pos2.z - width)
        local maxp = vector.new(pos2.x + width, pos2.y + 1, pos2.z + width)
        for z = minp.z, maxp.z do
            for y = minp.y, maxp.y do
                for x = minp.x, maxp.x do
                    local npos = vector.new(x, y, z)
                    local under = vector.new(npos.x, npos.y - 1, npos.z)
                    local is_walkable =
                        minetest.registered_nodes[minetest.get_node(under).name]
                            .walkable
                    if can_fit and is_walkable then
                        return npos
                    end
                end
            end
        end
    end
    return pos2
end

-------------
-- Mob API --
-------------

function draconis.is_stuck(self)
    if not mobkit.is_alive(self) then return end
    if not self.moveresult then return end
    local moveresult = self.moveresult
    if self.height < 1 then return false end
    for _, collision in ipairs(moveresult.collisions) do
        if collision.type == "node" then
            local pos = mobkit.get_stand_pos(self)
            local node_pos = collision.node_pos
            local yaw = self.object:get_yaw()
            local yaw_to_node = minetest.dir_to_yaw(vec_dir(pos, node_pos))
            if node_pos.y >= pos.y + 1
            and abs(diff(yaw, yaw_to_node)) <= 1.5 then
                local node = minetest.get_node(node_pos)
                if minetest.registered_nodes[node.name].walkable then
                    return true
                end
            end
        end
    end
    return false
end

function draconis.play_sound(self, sound)
    if self.time_from_last_sound < 6 then return end
	local params = self.sounds[sound]
	local param_table = {object = self.object}

	if #params > 0 then
		params = params[random(#params)]
	end

	param_table.gain = params.gain
	param_table.pitch = (params.pitch or 1) + (random(-5, 5) * 0.01)
    self.roar_anim_length = params.length
    self.time_from_last_sound = 0
    self.jaw_init = true
	return minetest.sound_play(params.name, param_table)
end

function draconis.handle_sounds(self)
    if self._anim
    and self._anim:find("fire") then
        return
    end
    local time_from_last_sound = self.time_from_last_sound
    if time_from_last_sound > 6 then
        local r = random(ceil(16 * self.growth_scale))
        if r < 2 then
            draconis.play_sound(self, "random")
        end
    end
end

function draconis.animate(self, anim)
	if self.animation and self.animation[anim] then
		if self._anim == anim then return end
        local old_anim = nil
        if self._anim then
            old_anim = self._anim
        end
		self._anim = anim

        local old_prty = 1
        if old_anim
        and self.animation[old_anim].prty then
            old_prty = self.animation[old_anim].prty
        end
        local prty = 1
        if self.animation[anim].prty then
            prty = self.animation[anim].prty
        end

		local aparms
		if #self.animation[anim] > 0 then
			aparms = self.animation[anim][random(#self.animation[anim])]
		else
			aparms = self.animation[anim]
		end

        aparms.frame_blend = aparms.frame_blend or 0
        if old_prty > prty then
            aparms.frame_blend = self.animation[old_anim].frame_blend or 0
        end

        self.anim_frame = -aparms.frame_blend
        self.frame_offset = 0

		self.object:set_animation(aparms.range, aparms.speed, aparms.frame_blend, aparms.loop)
	else
		self._anim = nil
	end
end

function draconis.get_head_pos(self, pos2)
    local pos = self.object:get_pos()
    pos.y = pos.y + 6 * self.growth_scale
    local yaw = self.object:get_yaw()
    local dir = vector.direction(pos, pos2)
    local yaw_diff = diff(yaw, minetest.dir_to_yaw(dir))
    if yaw_diff > 1 then
        local look_dir = minetest.yaw_to_dir(yaw + 1)
        dir.x = look_dir.x
        dir.z = look_dir.z
    elseif yaw_diff < -1 then
        local look_dir = minetest.yaw_to_dir(yaw - 1)
        dir.x = look_dir.x
        dir.z = look_dir.z
    end
    local head_yaw = yaw + (yaw_diff * 0.33)
    return vector.add(pos, vector.multiply(minetest.yaw_to_dir(head_yaw), (7 - abs(yaw_diff)) * self.growth_scale)), dir
end

local get_head_pos = draconis.get_head_pos

local fireworks_radius = 64
local ioncannon_radius = 50

function draconis.check_fireworks_hit(bang_pos, shield_breaker)
	if draconis.dragon ~= nil then
		local self = draconis.dragon
		if self.hit_debouncer > 0 then
			-- have to wait before you're allowed to hit it again.
			return
		end
		local dist = vec_dist(bang_pos, self.object:get_pos())
		local ydist = abs(bang_pos.y - self.object:get_pos().y)
		if shield_breaker and dist < fireworks_radius then
			if self.shielded then
				-- TODO: make a sound of shield breaking?
				cmsg.push_message_all(S("The barrier is broken, strike now!"))
				self.shield_regen_time = mobkit.remember(self, "shield_regen_time", 0)
				self.shielded = mobkit.remember(self, "shielded", false)
			end
			return
		end
		if self.name == "c_dragons:black_dragon" then
			-- Pff, not scared of fireworks!
			return
		end
		if dist < fireworks_radius and ydist < 13 then
			if self.shielded then
				-- TODO: make shield flash or something??
				return
			end
			flash_red(self)
			if self.greet_timer < 10 then
				-- HOW DARE YOU?!?
				self.current_phase = mobkit.remember(self, "current_phase", 4)
				-- TODO: agrier sound?
				dracons.play_sound(self, "roar")
			else
				self.current_phase = mobkit.remember(self, "current_phase", self.current_phase + 1)
				mobkit.make_sound(self, "hurt")
			end
			mobkit.hurt(self, 1)
			self.idle_timer = mobkit.remember(self, "idle_timer", 0)
			self.hit_debouncer = mobkit.remember(self, "hit_debouncer", 5)

			if self.hp <= 0 then
				local altar_pos = {x=self.altar_pos_x, y=self.altar_pos_y, z=self.altar_pos_z}
				for _,player in ipairs(minetest.get_connected_players()) do
					local player_pos = player:get_pos()
					local player_dist = vec_dist(altar_pos, player_pos)
					if player_dist < 128 then
						local meta = player:get_meta()
						local max_col = meta:get_int("max_col")
						if self.name == "c_dragons:blue_dragon" then
							local blue_colour_id = 4
							if max_col < blue_colour_id then
								cmsg.push_message_player(player, S("You have learned the blue colour."))
								-- TODO: particles? something?
								meta:set_int("max_col", blue_colour_id)
							end
						elseif self.name == "c_dragons:purple_dragon" then
							local purple_colour_id = 5
							if max_col < purple_colour_id then
								cmsg.push_message_player(player, S("You have learned the purple colour."))
								-- TODO: particles? something?
								meta:set_int("max_col", purple_colour_id)
							end
						end
					end
				end
				if self.name == "c_dragons:blue_dragon" then
					cmsg.push_message_all(S("Marundir is defeated!"))
					minetest.set_node(altar_pos, {name = "buildings:blue"})
				elseif self.name == "c_dragons:purple_dragon" then
					cmsg.push_message_all(S("Tyrirol is defeated!"))
					minetest.set_node(altar_pos, {name = "buildings:purple"})
				end
			elseif self.name == "c_dragons:purple_dragon" then
				-- TODO: make a sound for shield re-activating?
                                self.shield_regen_timer = mobkit.remember(self, "shield_regen_timer", 0)
                                self.shielded = mobkit.remember(self, "shielded", true)
			end
		end
	end
end

function draconis.check_ioncannon_hit(cannon_pos)
	if draconis.dragon ~= nil then
		local self = draconis.dragon
		if self.hit_debouncer > 0 then
			-- have to wait before you're allowed to hit it again.
			return
		end
		local flat_c_pos = {x=cannon_pos.x, y=0, z=cannon_pos.z}
		local dpos = self.object:get_pos()
		local flat_d_pos = {x=dpos.x, y=0, z=dpos.z}
		local flat_dist = vec_dist(flat_c_pos, flat_d_pos)
		if flat_dist < ioncannon_radius then
			if self.shielded then
				-- TODO: make shield flash or something??
				return
			end
			flash_red(self)
			if self.greet_timer < 10 then
				-- HOW DARE YOU?!?
				self.current_phase = mobkit.remember(self, "current_phase", 4)
				-- TODO: agrier sound?
				dracons.play_sound(self, "roar")
			else
				self.current_phase = mobkit.remember(self, "current_phase", self.current_phase + 1)
				mobkit.make_sound(self, "hurt")
			end
			mobkit.hurt(self, 1)
			self.idle_timer = mobkit.remember(self, "idle_timer", 0)
			self.hit_debouncer = mobkit.remember(self, "hit_debouncer", 5)

			if self.hp <= 0 then
				local altar_pos = {x=self.altar_pos_x, y=self.altar_pos_y, z=self.altar_pos_z}
				for _,player in ipairs(minetest.get_connected_players()) do
					local player_pos = player:get_pos()
					local player_dist = vec_dist(altar_pos, player_pos)
					if player_dist < 128 then
						local meta = player:get_meta()
						local max_col = meta:get_int("max_col")
						if self.name == "c_dragons:blue_dragon" then
							local blue_colour_id = 4
							if max_col < blue_colour_id then
								cmsg.push_message_player(player, S("You have learned the blue colour."))
								-- TODO: particles? something?
								meta:set_int("max_col", blue_colour_id)
							end
						elseif self.name == "c_dragons:purple_dragon" then
							local purple_colour_id = 5
							if max_col < purple_colour_id then
								cmsg.push_message_player(player, S("You have learned the purple colour."))
								-- TODO: particles? something?
								meta:set_int("max_col", purple_colour_id)
							end
						elseif self.name == "c_dragons:black_dragon" then
							local black_colour_id = 6
							if max_col < black_colour_id then
								cmsg.push_message_player(player, S("You have learned the black colour."))
								-- TODO: particles? something?
								meta:set_int("max_col", black_colour_id)
							end
						end
					end
				end
				if self.name == "c_dragons:blue_dragon" then
					cmsg.push_message_all(S("Marundir is defeated!"))
					minetest.set_node(altar_pos, {name = "buildings:blue"})
				elseif self.name == "c_dragons:purple_dragon" then
					cmsg.push_message_all(S("Tyrirol is defeated!"))
					minetest.set_node(altar_pos, {name = "buildings:purple"})
				elseif self.name == "c_dragons:black_dragon" then
					cmsg.push_message_all(S("Nowal is defeated!"))
					minetest.set_node(altar_pos, {name = "buildings:black"})
				end
			elseif self.name == "c_dragons:purple_dragon" or self.name == "c_dragons:black_dragon" then
				-- TODO: make a sound for shield re-activating?
                                self.shield_regen_timer = mobkit.remember(self, "shield_regen_timer", 0)
                                self.shielded = mobkit.remember(self, "shielded", true)
			end
		end
	end
end

-- Dynamic Animation --

local function clamp_bone_rot(n) -- Fixes issues with bones jittering when yaw clamps
    if n < -180 then
        n = n + 360
    elseif n > 180 then
        n = n - 360
    end
    if n < -60 then
        n = -60
    elseif n > 60 then
        n = 60
    end
    return n
end

local function interp_bone_rot(a, b, w) -- Smoothens bone movement
    local pi = math.pi
    if math.abs(a - b) > math.deg(pi) then
        if a < b then
            return ((a + (b - a) * w) + (math.deg(pi) * 2))
        elseif a > b then
            return ((a + (b - a) * w) - (math.deg(pi) * 2)) 
        end
    end
    return a + (b - a) * w
end

function draconis.open_jaw(self)
    if not self._anim then return end
    if self.jaw_init then
        if self._anim:find("fire") then
            self.jaw_init = false
            self.roar_anim_length = 0
            return
        end
        local _, rot = self.object:get_bone_position("Jaw.CTRL")
        local b_rot = interp_bone_rot(rot.x, -45, 0.2)
        self.object:set_bone_position("Jaw.CTRL", {x=0,y=0.455,z=-0.255}, {x=b_rot,y=0,z=0})
        self.roar_anim_length = self.roar_anim_length - self.dtime
        if floor(rot.x) == -45
        and self.roar_anim_length <= 0 then
            self.jaw_init = false
            self.roar_anim_length = 0
        end
    else
        local _, rot = self.object:get_bone_position("Jaw.CTRL")
        local b_rot = interp_bone_rot(rot.x, 0, self.dtime * 3)
        self.object:set_bone_position("Jaw.CTRL", {x=0,y=0.455,z=-0.255}, {x=b_rot,y=0,z=0})
    end
end

function draconis.move_tail(self)
    local tyaw = self._tyaw
    if self._anim == "stand"
    or self._anim == "stand_fire"
    or self._anim == "fly_idle"
    or self._anim == "fly_idle_fire" then
        tyaw = self.object:get_yaw()
    end
    local yaw = self.object:get_yaw()
    for seg = 1, #self.dynamic_anim_data.tail do
        local data = self.dynamic_anim_data.tail[seg]
        local _, rot = self.object:get_bone_position("Tail.".. seg .. ".CTRL")
        rot = rot.z
        local tgt_rot = clamp_bone_rot(-math.deg(yaw - tyaw)) * self.dynamic_anim_data.swing_factor
        local new_rot = 0
        if self.dtime then
            new_rot = interp_bone_rot(rot, tgt_rot, self.dtime * 1.5)
        end
        self.object:set_bone_position("Tail.".. seg .. ".CTRL", data.pos, {x = data.rot.x, y = data.rot.y, z = new_rot * data.rot.z})
    end
end

function draconis.move_head(self, tyaw, pitch)
    local yaw = self.object:get_yaw()
    for seg = 1, #self.dynamic_anim_data.head do
        local seg_no = #self.dynamic_anim_data.head
        local data = self.dynamic_anim_data.head[seg]
        local bone_name = "Neck.".. seg .. ".CTRL"
        if seg == seg_no then
            bone_name = "Head.CTRL"
        end
        local _, rot = self.object:get_bone_position(bone_name)
        local look_yaw = clamp_bone_rot(math.deg(yaw - tyaw))
        local look_pitch = data.rot.x
        if pitch then
            look_pitch = clamp_bone_rot(math.deg(pitch)) * data.pitch_factor
        end
        if tyaw ~= yaw then
            look_yaw = look_yaw * self.dynamic_anim_data.yaw_factor
        end
        local bone_yaw = look_yaw
        local bone_pitch = look_pitch + (data.pitch_offset or 0)
        if self.jaw_init
        and data.bite_angle then
            look_pitch = look_pitch + data.bite_angle
        end
        if self.dtime then
            bone_yaw = interp_bone_rot(rot.z, look_yaw, self.dtime * 1.5)
            bone_pitch = interp_bone_rot(rot.x, look_pitch + (data.pitch_offset or 0), self.dtime * 1.5)
        end
        self.object:set_bone_position(bone_name, data.pos, {x = bone_pitch, y = data.rot.y, z = bone_yaw})
    end
end

-- Dragon Breath --

local function scorch_nodes(pos, radius)
    local h_stride = random(-radius, radius)
    local v_stride = random(-radius, math.ceil(radius * 0.5))
    local npos = {
        x = pos.x + h_stride,
        y = pos.y + v_stride,
        z = pos.z + h_stride
    }
    if minetest.is_protected(npos, "") then
        return
    end
    local name = minetest.get_node(npos).name
    if name
    and name ~= "air"
    and name ~= "ignore" then
        if draconis.find_value_in_table(draconis.walkable_nodes, name) then
            local above = vector.new(npos.x, npos.y + 1, npos.z)
            if minetest.get_node(above).name == "air" then
                minetest.set_node(above, {name = "fire:basic_flame"})
            end
        end
    end
end

local function blue_nodes(pos, radius)
	local h_stride = random(-radius, radius)
	local v_stride = random(-radius, math.ceil(radius * 0.5))
	local npos = {
		x = pos.x + h_stride,
		y = pos.y + v_stride,
		z = pos.z + h_stride
	}
	if minetest.is_protected(npos, "") then
		return
	end
	local node = minetest.get_node(npos)
	if node and node.name ~= "air" then
		local def = minetest.registered_nodes[node.name]
		if def.groups.building then
			-- Not even dragons can do that :<
			return
		end
		minetest.set_node(npos, {name = "world:blue"} )
		if math.random(1, 3) == 1 then
			local new_node = minetest.get_node_or_nil(npos)
			if new_node then
				-- Yes, it's magic
				colours.trigger(new_node, npos, 0)
			end
		end
	end
end

local function purple_nodes(pos, radius)
	local h_stride = random(-radius, radius)
	local v_stride = random(-radius, math.ceil(radius * 0.5))
	local npos = {
		x = pos.x + h_stride,
		y = pos.y + v_stride,
		z = pos.z + h_stride
	}
	if minetest.is_protected(npos, "") then
		return
	end
	local node = minetest.get_node(npos)
	if node and node.name ~= "air" then
		local def = minetest.registered_nodes[node.name]
		if def.groups.building then
			-- Not even dragons can do that :<
			return
		end
		minetest.set_node(npos, {name = "world:purple"} )
		if math.random(1, 3) == 1 then
			local new_node = minetest.get_node_or_nil(npos)
			if new_node then
				-- Yes, it's magic
				colours.trigger(new_node, npos, math.random(1, 4))
			end
		end
	end
end

local function black_nodes(pos, radius)
	local h_stride = random(-radius, radius)
	local v_stride = random(-radius, math.ceil(radius * 0.5))
	local npos = {
		x = pos.x + h_stride,
		y = pos.y + v_stride,
		z = pos.z + h_stride
	}
	if minetest.is_protected(npos, "") then
		return
	end
	local node = minetest.get_node(npos)
	if node and node.name ~= "air" then
		local def = minetest.registered_nodes[node.name]
		if def.groups.building then
			-- Not even dragons can do that :<
			return
		end
		minetest.set_node(npos, {name = "world:black"} )
		if math.random(1, 3) == 1 then
			local new_node = minetest.get_node_or_nil(npos)
			if new_node then
				-- Yes, it's magic
				colours.trigger(new_node, npos, 0)
			end
		end
	end
end

local function breath_sound(self, sound)
    if not self.breath_timer then self.breath_timer = 0.1 end
    self.breath_timer = self.breath_timer - self.dtime
    if self.breath_timer <= 0 then
        self.breath_timer = 2
        minetest.sound_play(sound,{
            object = self.object,
            gain = 1.0,
            max_hear_distance = 64,
            loop = false,
        })
    end
end

local function get_line(pos1, pos2)
    local steps = floor(vec_dist(pos1, pos2))
    local line = {}

    for i = 0, steps do
        local pos

        if steps > 0 then
            pos = {
                x = pos1.x + (pos2.x - pos1.x) * (i / steps),
                y = pos1.y + (pos2.y - pos1.y) * (i / steps),
                z = pos1.z + (pos2.z - pos1.z) * (i / steps)
            }
        else
            pos = pos1
        end
        table.insert(line, pos)
    end
    table.sort(line, function(a, b) return vec_dist(a, pos2) > vec_dist(b, pos2) end)
    if #line < 1 then
        return nil
    else
        local halt = nil
        for i = #line, 1, -1 do
            local node = minetest.get_node(line[i])
            if minetest.registered_nodes[node.name].walkable
            and mobkit.get_node_height(line[i]) > 4.5 then
                if not halt then
                    halt = i
                    table.remove(line, i)
                elseif i > halt then
                    table.remove(line, i)
                end
            end
        end
    end
    return line, vec_dist(pos1, line[#line])
end

function draconis.blue_breath(self, goal, range)
	breath_sound(self, "draconis_beam_breath")
	local pos
	local dir
	pos, dir = get_head_pos(self, goal)
	dir.y = vec_dir(pos, goal).y
	pos.y = pos.y + self.object:get_rotation().x
	local dest = vector.add(pos, vector.multiply(dir, range))
	local path, length = get_line(pos, dest)
	minetest.add_particlespawner({
		amount = 10,
		time = 0.5,
		minpos = vector.add(path[1], vector.multiply(self.object:get_velocity(), 0.22)),
		maxpos = vector.add(path[1], vector.multiply(self.object:get_velocity(), 0.22)),
		minvel = vector.multiply(dir, 32),
		maxvel = vector.multiply(dir, 48),
		minacc = {x = -4, y = -4, z = -4},
		maxacc = {x = 4, y = 4, z = 4},
		minexptime = 0.02 * length,
		maxexptime = 0.04 * length,
		minsize = 8 * self.growth_scale,
		maxsize = 12 * self.growth_scale,
		collisiondetection = true,
		vertical = false,
		glow = 8,
		texture = "draconis_blue_particle_" .. random(1, 3) .. ".png"
	})
	local scale_factor = ceil(4 * clamp(self.growth_scale, 0.25, 1.5))
	if scale_factor < #path then
		for i = 1, #path, ceil(#path / scale_factor) do
			blue_nodes(path[i], scale_factor)
		end
	else
		blue_nodes(path[#path], scale_factor)
	end
end

function draconis.purple_breath(self, goal, range)
	breath_sound(self, "draconis_beam_breath")
	local pos
	local dir
	pos, dir = get_head_pos(self, goal)
	dir.y = vec_dir(pos, goal).y
	pos.y = pos.y + self.object:get_rotation().x
	local dest = vector.add(pos, vector.multiply(dir, range))
	local path, length = get_line(pos, dest)
	minetest.add_particlespawner({
		amount = 10,
		time = 0.5,
		minpos = vector.add(path[1], vector.multiply(self.object:get_velocity(), 0.22)),
		maxpos = vector.add(path[1], vector.multiply(self.object:get_velocity(), 0.22)),
		minvel = vector.multiply(dir, 32),
		maxvel = vector.multiply(dir, 48),
		minacc = {x = -4, y = -4, z = -4},
		maxacc = {x = 4, y = 4, z = 4},
		minexptime = 0.02 * length,
		maxexptime = 0.04 * length,
		minsize = 8 * self.growth_scale,
		maxsize = 12 * self.growth_scale,
		collisiondetection = true,
		vertical = false,
		glow = 8,
		texture = "draconis_purple_particle_" .. random(1, 3) .. ".png"
	})
	local scale_factor = ceil(4 * clamp(self.growth_scale, 0.25, 1.5))
	if scale_factor < #path then
		for i = 1, #path, ceil(#path / scale_factor) do
			purple_nodes(path[i], scale_factor)
		end
	else
		purple_nodes(path[#path], scale_factor)
	end
end

function draconis.black_breath(self, goal, range)
	breath_sound(self, "draconis_beam_breath")
	local pos
	local dir
	pos, dir = get_head_pos(self, goal)
	dir.y = vec_dir(pos, goal).y
	pos.y = pos.y + self.object:get_rotation().x
	local dest = vector.add(pos, vector.multiply(dir, range))
	local path, length = get_line(pos, dest)
	minetest.add_particlespawner({
		amount = 10,
		time = 0.5,
		minpos = vector.add(path[1], vector.multiply(self.object:get_velocity(), 0.22)),
		maxpos = vector.add(path[1], vector.multiply(self.object:get_velocity(), 0.22)),
		minvel = vector.multiply(dir, 32),
		maxvel = vector.multiply(dir, 48),
		minacc = {x = -4, y = -4, z = -4},
		maxacc = {x = 4, y = 4, z = 4},
		minexptime = 0.02 * length,
		maxexptime = 0.04 * length,
		minsize = 8 * self.growth_scale,
		maxsize = 12 * self.growth_scale,
		collisiondetection = true,
		vertical = false,
		glow = 8,
		texture = "draconis_black_particle_" .. random(1, 3) .. ".png"
	})
	local scale_factor = ceil(4 * clamp(self.growth_scale, 0.25, 1.5))
	if scale_factor < #path then
		for i = 1, #path, ceil(#path / scale_factor) do
			black_nodes(path[i], scale_factor)
		end
	else
		black_nodes(path[#path], scale_factor)
	end
end
