bweapons = {}
local players = {}

--Global settings
local damage_multiplier = minetest.settings:get("bweapons_damage_multiplier") or 1
local combine_velocity = minetest.settings:get("bweapons_combine_velocity") or false
local projectile_raycast_dist = minetest.settings:get("bweapons_projectile_raycast_distance") or 0.5

--Utility functions
minetest.register_on_joinplayer(function(player)
    local name = player:get_player_name()
    players[name] = {reloading=false}
    end
)

minetest.register_on_leaveplayer(function(player)
    local name = player:get_player_name()
    players[name] = nil
    end
)

local function reset_player_cooldown(user)
    if user ~= nil then
        local name = user:get_player_name()
        players[name] = {reloading=false}
    end
end

function bweapons.get_spread(spread)
    return (math.random(-32768, 32768)/65536)*spread
end

-- Helper function that makes projectiles ignore other projectiles
-- and dropped items
-- 't' should be an ObjectRef, not the pointed_thing object
local function valid_target(t)
    assert(t ~= nil, "t in valid_target is nil")
    assert(t.ref == nil, "this is a pointed_thing, not an ObjectRef")

    local ent = t:get_luaentity()

    -- See: https://github.com/minetest/minetest/issues/13297
    -- Skip over invalid objects
    if not ent or t:get_pos() == nil then
        return false
    end

    -- Projectiles should not destroy dropped items, health bars, or boats
    if ent and (ent.name == "__builtin:item" or
                ent.name == "visual_harm_1ndicators:hpbar" or
                ent.name == "boats:boat") then
        return false
    end

    -- Projectiles should ignore other projectiles
    if ent and (ent._yams_mob_projectile or ent._yams_player_projectile) then
        return false
    end

    return true
end

-- Note: has a side effect if the player has logged out
local function owner_exists(proj)
    -- Detect if the player left the game and replace the owner field with
    -- itself; rather ugly hack but oh well
    if proj.owner ~= proj.object and proj.owner:get_player_name() == "" then
        -- The Mobs Redo code will be able to handle the case where the hitter
        -- is not a player, but it might not be able to handle nil
        proj.owner = proj.object
    end

    return proj.owner ~= proj.object
end

-- Handles the edge case in which damage falls between 0 and 1
-- The rule in yams is that the minimum damage of an attack is always 1 unless
-- the entity receiving the attack is immune (having 100% resistance) to it
local function damage_helper(value)
    if value == 0 then
        -- Immune to the attack
        return value
    elseif value > 0 and value < 1 then
        -- Heavily resistant but not immune
        return 1
    elseif value < 0 and value > -1 then
        -- Absorbs the damage but the amount of healing would be less than 1
        return -1
    else
        -- Do normal probability-based rounding
        return yams.round_with_fraction_as_probability(value)
    end
end

function bweapons.damage_aoe(damage, puncher, pos, radius, proj)
    local targs = minetest.get_objects_inside_radius(pos, radius)
    for _, t in pairs(targs) do
        if valid_target(t) then
            local tpos = t:get_pos()

            -- We have to do another raycast here because 'tpos' is the center
            -- of the mob, but if the mob is big then the sides of its selection
            -- box are farther from the center, and if we do not account for
            -- that then larger mobs will receive less damage from direct hits
            -- than smaller mobs; instead we need to get the point on the
            -- selection box that is closest to the point of impact for each
            -- entity and use that when calculating distance, and the easiest
            -- way to obtain that point is to do another raycast and see where
            -- the ray intersects the box
            local blocked = nil

            for thing in minetest.raycast(pos, tpos) do
                local ipoint = thing.intersection_point
                if thing.type == "node" then
                    -- We have to check whether the mob is behind a wall and
                    -- stop if that is the case, but a simple line of sight
                    -- check does not work because the node that the projectile
                    -- hit counts as blocking line of sight, which breaks
                    -- the common case of hitting a floor to damage multiple
                    -- mobs around the point of impact; to address this, we go
                    -- towards the mob slightly and check if we are still inside
                    -- a node or not
                    local dir = vector.direction(ipoint, tpos)
                    local offset = vector.copy(ipoint) + dir * 0.25

                    local nodename = minetest.get_node(offset).name
                    local nodedef = minetest.registered_nodes[nodename]

                    -- YAMS-TODO: account for the 'liquids_stop' field
                    if nodedef and nodedef.walkable then
                        blocked = true
                        break
                    end
                elseif thing.type == "object" and thing.ref == t then
                    tpos = ipoint
                    blocked = false
                    break
                end
            end

            assert(blocked ~= nil, "damage_aoe: should not happen")

            if not blocked then
                -- Note that direct hits should have a distance of 0
                local dist = vector.distance(pos, tpos)
                if dist < 0.01 then  -- Account for a bit of error
                    dist = 0
                end

                local dist_damage = damage / (1 + dist)

                if minetest.get_modpath("yams_rpg") and owner_exists(proj) then
                    dist_damage = yams.calc_player_damage(proj.owner, t, dist_damage,
                                  proj._yams_stat_factors, proj._yams_elements)
                    dist_damage = damage_helper(dist_damage)
                end

                local dist_knockback = math.min(damage, 10)  -- cap at 10 just in case
                if proj.knockback ~= nil then
                    local dist_knockback = proj.knockback / (1 + dist)
                end

                local dir = vector.direction(pos, tpos)
                local theta = math.atan2(dir.z, dir.x)

                -- Check if the target is a Mobs Redo mob
                -- This is a hack that enables passing in additional
                -- information about the projectile beyond what tool
                -- capabilities can accomodate
                -- Yes, I copied and pasted this oh well
                local ent = t:get_luaentity()
                if ent and ent._cmi_is_mob and ent.yams_arrow_punch then
                    local data = {angle = theta,
                                  knockback = dist_knockback,
                                  elements = proj._yams_elements,
                                  proj_name = proj.name}
                    ent:yams_arrow_punch(puncher, 1.0, {
                        full_punch_interval = 1.0,
                        damage_groups = {fleshy = dist_damage},
                        }, nil, data)
                else
                    t:punch(puncher, 1.0, {
                        full_punch_interval = 1.0,
                        damage_groups = {fleshy = dist_damage},
                    }, nil)
                end
            end
        end
    end
end

--Utility projectile registration function
local function register_projectile(def)

    local aglow = 0

    if def.glow then
        aglow = 14
    end

    --Main projectile registration function
    minetest.register_entity(def.name, {
        initial_properties = {
            physical = false,
            is_visible = true,
            static_save = false,
            visual = "sprite",
            visual_size = {x = def.visual_size, y = def.visual_size, z = def.visual_size},
            textures = {def.texture},
            glow = aglow,
            pointable = false,
        },

        --Internal variables
        timer = 0,
        owner = "",
        node_hit = false,
        previous_pos = {},

        -- Variables for yams
        _yams_knockback = def.knockback,
        _yams_stat_factors = def.stat_factors,
        _yams_elements = def.elements,
        _yams_player_projectile = true,

        --Performing checks every server step
        on_step = function(self, dtime)

            self.timer = self.timer + dtime
            local pos = self.object:get_pos()

            --Trail particle spawner
            if def.trail_particle then
                minetest.add_particlespawner({
                    amount = def.trail_particle_amount,
                    time = 0.05,
                    minpos = {x=pos.x-def.trail_particle_displacement, y=pos.y-def.trail_particle_displacement, z=pos.z-def.trail_particle_displacement},
                    maxpos = {x=pos.x+def.trail_particle_displacement, y=pos.y+def.trail_particle_displacement, z=pos.z+def.trail_particle_displacement},
                    minvel = {x=-def.trail_particle_velocity, y=-def.trail_particle_velocity, z=-def.trail_particle_velocity},
                    maxvel = {x=def.trail_particle_velocity, y=def.trail_particle_velocity, z=def.trail_particle_velocity},
                    minacc = {x=0, y=def.trail_particle_gravity, z=0},
                    maxacc = {x=0, y=def.trail_particle_gravity, z=0},
                    minexptime = 1,
                    maxexptime = 3,
                    minsize = def.trail_particle_min_size,
                    maxsize = def.trail_particle_size,
                    texture = def.trail_particle,
                    glow=def.trail_particle_glow,
                })
            end

            --Hit detection
            local ray = minetest.raycast(self.previous_pos, pos, true, def.liquids_stop)
            local target = ray:next()

            -- Get the next item hit if the object is not a valid target or if
            -- the node hit is not supposed to stop the projectile
            while target do
                if target and target.type == "object" and (target.ref == self.owner
                        or not valid_target(target.ref)) then
                    target = ray:next()
                elseif target and target.type == "node" then
                    local blocking = false
                    local node = minetest.get_node_or_nil(target.under)
                    local mrn = minetest.registered_nodes
                    if node and mrn[node.name] and (mrn[node.name].walkable or
                            def.liquids_stop and mrn[node.name].liquidtype ~= "none") then
                        blocking = true
                    end

                    if blocking then
                        break
                    else
                        target = ray:next()
                    end
                else
                    break
                end
            end

            if target and target.type == "node" then
                local node = minetest.get_node_or_nil(minetest.get_pointed_thing_position(target, false))
                if node and minetest.registered_nodes[node.name] and (minetest.registered_nodes[node.name].walkable or
                def.liquids_stop and minetest.registered_nodes[node.name].liquidtype ~= "none") then
                    if def.aoe then
                        -- yams.calc_player_damage is called within bweapons.damage_aoe
                        bweapons.damage_aoe(def.damage, self.owner, target.intersection_point,
                                            def.aoe_radius, self)
                    end
                    if def.on_hit then
                        def.on_hit(self, target)
                    end
                    self.node_hit = true
                end
            elseif target and target.type == "object" and valid_target(target.ref) then
                if def.aoe then
                    -- yams.calc_player_damage is called within bweapons.damage_aoe
                    bweapons.damage_aoe(def.damage, self.owner, target.intersection_point,
                                        def.aoe_radius, self)
                else
                    -- Calculate damage here so that the object that gets hit does
                    -- not have to calculate its own damage
                    local damage = def.damage
                    if minetest.get_modpath("yams_rpg") and owner_exists(self) then
                        damage = yams.calc_player_damage(self.owner, target.ref, damage,
                                 self._yams_stat_factors, self._yams_elements)
                        damage = damage_helper(damage)
                    end

                    -- Check if the target is a Mobs Redo mob
                    -- This is a hack that enables passing in additional
                    -- information about the projectile beyond what tool
                    -- capabilities can accomodate
                    local ent = target.ref:get_luaentity()
                    if ent and ent._cmi_is_mob and ent.yams_arrow_punch then
                        -- Calculate the angle manually because the API is
                        -- bugged and the distance parameter doesn't work
                        -- See comments in the yams_rpg mod
                        local dir = vector.normalize(self.object:get_velocity())
                        local theta = math.atan2(dir.z, dir.x)
                        local data = {angle = theta,
                                      knockback = self._yams_knockback,
                                      elements = self._yams_elements,
                                      proj_name = self.name}

                        ent:yams_arrow_punch(self.owner, 1.0, {
                            full_punch_interval = 1.0,
                            damage_groups = {fleshy = damage},
                            }, nil, data)
                    else
                        target.ref:punch(self.owner, 1.0, {
                        full_punch_interval = 1.0,
                        damage_groups = {fleshy = damage},
                        }, nil)
                    end
                end
                if def.on_hit then
                    def.on_hit(self, target)
                end
                self.node_hit = true
            end

            --Spawn hit particles, execute on_hit if defined, drop an item if defined and remove the object
            if self.node_hit then
                --Little hit flares
                if def.hit_flare then
                    minetest.add_particle({
                        pos = target.intersection_point,
                        expirationtime = 0.2,
                        size = def.hit_flare_size,
                        collisiondetection = false,
                        vertical = false,
                        texture = def.hit_flare,
                        glow = def.hit_flare_glow,
                })
                end
                --Hit particles
                if def.hit_particle then
                    minetest.add_particlespawner({
                        amount = def.hit_particle_amount,
                        time = 0.05,
                        minpos = target.intersection_point,
                        maxpos = target.intersection_point,
                        minvel = {x=-def.hit_particle_velocity, y=-def.hit_particle_velocity, z=-def.hit_particle_velocity},
                        maxvel = {x=def.hit_particle_velocity, y=def.hit_particle_velocity+1, z=def.hit_particle_velocity},
                        minacc = {x=0, y=def.hit_particle_gravity, z=0},
                        maxacc = {x=0, y=def.hit_particle_gravity, z=0},
                        minexptime = 2,
                        maxexptime = 4,
                        minsize = def.hit_particle_min_size,
                        maxsize = def.hit_particle_size,
                        collisiondetection = false,
                        collision_removal = false,
                        object_collision = false,
                        vertical = false,
                        texture = def.hit_particle,
                        glow = def.hit_particle_glow,
                        })
                end
                --Hit sound
                if def.hit_sound then
                    minetest.sound_play(def.hit_sound, {pos=target.intersection_point, gain=def.hit_sound_gain, max_hear_distance=2*64})
                end
                --Drop a drop item, if defined
                if def.drop then
                    if math.random() < def.drop_chance then
                        -- YAMS-TODO: make the dropped item's location better
                        minetest.add_item(target.intersection_point, def.drop)
                    end
                end
                --Remove the projectile
                self.object:remove()
            end

            --Spawn a hit flare and remove the projectile if it timeouts
            if self.timer > def.timeout then
                if def.flare then
                    minetest.add_particle({
                        pos = pos,
                        expirationtime = 0.2,
                        size = math.floor(def.flare_size/2),
                        collisiondetection = false,
                        vertical = false,
                        texture = def.flare,
                        glow = def.flare_glow,
                    })
                end
                if def.on_timeout then def.on_timeout(self) end
                self.object:remove()
            end

            self.previous_pos = pos

        end,
    })
end

local function bweapons_repair(itemstack, player, old_craft_grid, craft_inv, name, repair_with, uses, repair_uses)
    if itemstack == nil or itemstack:get_name() ~= name then return end
    local w_count = 0
    local r_count = 0
    local modstack

    for _,v in pairs(old_craft_grid) do
        if v:get_name() == name then
            w_count = w_count + 1
            modstack = v
        elseif v:get_name() == repair_with then
            r_count = r_count + 1
        end
    end

    if w_count == 1 and r_count == 1 then
        local wear = modstack:get_wear()
        if wear > 0 then
            wear = wear - (65535/uses * repair_uses)
            if wear < 0 then wear = 0 end
            modstack:set_wear(wear)
            return modstack
        end
    end

end

function bweapons.register_weapon(def)

    --Stop registration if mod requirements are not met or not enough definition fields
    if def.has_durability and not minetest.get_modpath("technic") and def.requires_technic then
        minetest.log("error", "Technic modpack is required for technic-powered weapons!")
        return
    end

    if def.mana_per_use and not minetest.get_modpath("mana") then
        minetest.log("error", "Mana required for mana-based weapons!")
        return
    end

    if def.name == nil or def.description == nil or def.texture == nil then
        minetest.log("error", "Missing fields in weapon registration! Check api documentation")
        return
    end

    --Setting defaults if fields are not defined in definition
    local damage = def.damage or 1
    local technic_charge = def.technic_charge or 100000
    local uses = def.uses or 50
    local technic_charge_per_use = technic_charge/uses
    local hitscan = def.hitscan or false
    local spawndist = def.spawn_distance or 1
    local shot_amount = def.shot_amount or 1
    local spread = def.spread or 0
    local cooldown = def.cooldown or 0
    local liquids_stop = def.liquids_stop or false
    local distance = def.distance or 10
    local penetration = def.penetration or 1
    local aoe = def.aoe or false
    local aoe_radius = def.aoe_radius or 5
    local flare_size = def.flare_size or 10
    local flare_glow = 0
    local hit_flare_size = def.hit_flare_size or 0.5
    local hit_flare_glow = 0
    local ammo_per_shot = def.ammo_per_shot or 1
    local repair_uses = def.repair_uses or 8
    local hit_particle_velocity = def.hit_particle_velocity or 2
    local hit_particle_gravity = def.hit_particle_gravity or -10
    local hit_particle_size = def.hit_particle_size or 3
    local hit_particle_min_size = math.floor(hit_particle_size/2)
    local hit_particle_amount = def.hit_particle_amount or 32
    local hit_particle_glow = 0
    local drop_chance = def.drop_chance or 0.9
    local fire_sound_gain = def.fire_sound_gain or 1
    local hit_sound_gain = def.hit_sound_gain or 0.25
    local reload_sound_gain = def.reload_sound_gain or 0.25
    local trail_particle_distance = def.hitscan_particle_distance or 0.5
    local trail_particle_velocity = def.trail_particle_velocity or 1
    local trail_particle_gravity = def.trail_particle_gravity or 0
    local trail_particle_size = def.trail_particle_size or 1
    local trail_particle_min_size = math.floor(trail_particle_size/2)
    local trail_particle_amount = def.trail_particle_amount or 4
    local trail_particle_displacement = def.trail_particle_displacement or 0.5
    local trail_particle_glow = 0
    local tool_groups = {}

    local craft_shape = "shaped"

    if def.shapeless_recipe then
        craft_shape = "shapeless"
    end

    if def.flare_glow then
        flare_glow = 14
    end

    if def.hit_particle_glow then
        hit_particle_glow = 14
    end

    if def.hit_flare_glow then
        hit_flare_glow = 14
    end

    if def.trail_particle_glow then
        trail_particle_glow = 14
    end

    damage = damage * damage_multiplier

    local projectile_speed = def.projectile_speed or 15
    local projectile_gravity = def.projectile_gravity or 0
    local projectile_timeout = def.projectile_timeout or 1.0
    local projectile_texture = def.projectile_texture or "bweapons_api_missing_texture.png"
    local projectile_glow = def.projectile_glow or false
    local projectile_visual_size = def.projectile_visual_size or 1

    local on_refill = nil
    local wear_represents = "mechanical_wear"

    if def.requires_technic then
        on_refill = technic.refill_RE_charge
        wear_represents = "technic_RE_charge"
    elseif def.custom_charge then
        wear_represents = "bweapons_custom_charge"
    end

    if not def.tool_repair then
        tool_groups["disable_repair"] = 1
    end

    -- Additions for yams
    local knockback = def.knockback or 6.0  -- Same as a copper sword

    -- If an item is repaired by using another item and anvil repair is not
    -- explicitly enabled, then disable anvil repair
    if def.anvil_repair == nil and def.repair_with then
        tool_groups["not_repaired_by_anvil"] = 1
    end
    -- Anvil repair can also be disabled explicitly
    if def.anvil_repair == false then
        tool_groups["not_repaired_by_anvil"] = 1
    end

    --Make a projectile definition and register projectile for the weapon, if hitscan=false
    if not hitscan then
        local projectiledef = {
            name=def.name.."_projectile",
            damage=damage,
            timeout=projectile_timeout,
            aoe=aoe,
            aoe_radius=aoe_radius,
            visual_size=projectile_visual_size,
            texture=projectile_texture,
            glow=projectile_glow,
            trail_particle=def.trail_particle,
            trail_particle_velocity=trail_particle_velocity,
            trail_particle_gravity=trail_particle_gravity,
            trail_particle_glow=trail_particle_glow,
            trail_particle_size=trail_particle_size,
            trail_particle_min_size=trail_particle_min_size,
            trail_particle_amount=trail_particle_amount,
            trail_particle_displacement=trail_particle_displacement,
            flare=def.flare,
            flare_size=flare_size,
            flare_glow=flare_glow,
            hit_flare=def.hit_flare,
            hit_flare_size=hit_flare_size,
            hit_flare_glow=hit_flare_glow,
            hit_particle=def.hit_particle,
            hit_particle_glow=hit_particle_glow,
            hit_particle_velocity=hit_particle_velocity,
            hit_particle_size=hit_particle_size,
            hit_particle_gravity=hit_particle_gravity,
            hit_particle_min_size=hit_particle_min_size,
            hit_particle_amount=hit_particle_amount,
            hit_sound=def.hit_sound,
            hit_sound_gain=hit_sound_gain,
            drop=def.drop,
            drop_chance=drop_chance,
            on_hit=def.on_hit,
            on_timeout=def.on_timeout,
            liquids_stop=liquids_stop,
            knockback=knockback,
            stat_factors=def.stat_factors,
            elements=def.elements
            }
        register_projectile(projectiledef)
    end

    --Main tool registration function
    minetest.register_tool(def.name, {
        description = def.description,
        inventory_image = def.texture,
        wield_scale = 1,
        stack_max = 1,
        groups = tool_groups,
        wear_represents = wear_represents,
        on_refill = on_refill,
        _tt_help = def._tt_help,
        _yams_knockback = knockback,  -- Only used for tooltips
        _yams_stat_factors = def.stat_factors,  -- Only used for tooltips
        _yams_elements = def.elements,  -- Only used for tooltips
        _yams_tt_damage = damage,
        _yams_tt_mana_cost = def.mana_per_use,
        _yams_tt_ammo_type = def.ammo_type,
        _yams_tt_repair_with = def.repair_with,
        on_use = function(itemstack, user, pointed_thing)

            if not user then return end

            local playername = user:get_player_name()
            local inv = user:get_inventory()

            --Return if player is reloading, not enough mana (if defined) or no ammo (if defined)
            if
            players[playername]["reloading"] or
            def.mana_per_use and mana.get(playername) < def.mana_per_use or
            def.ammo_type and not inv:contains_item("main", {name=def.ammo_type, count=ammo_per_shot})
            then
                if def.reload_sound then
                    minetest.sound_play(def.reload_sound, {object=user, gain=reload_sound_gain, max_hear_distance=2*64})
                end
                return
            end

            if def.ammo_type then
                local hotbar_max = user:hud_get_hotbar_itemcount()
                local found = false
                for i = 1, hotbar_max do
                    local stack = inv:get_stack("main", i)
                    local count = stack:get_count()

                    if stack:get_name() == def.ammo_type and count >= ammo_per_shot then
                        stack:set_count(count - ammo_per_shot)
                        inv:set_stack("main", i, stack)
                        found = true
                        break
                    end
                end

                if not found then
                    if def.reload_sound then
                        minetest.sound_play(def.reload_sound, {object=user, gain=reload_sound_gain, max_hear_distance=2*64})
                    end
                    -- YAMS-TODO: translate
                    minetest.chat_send_player(playername, "Ammo not in hotbar!")
                    return
                end
            end

            if def.has_durability then
                local meta = minetest.deserialize(itemstack:get_metadata())
                if
                def.requires_technic and not meta or
                def.requires_technic and meta.charge < technic_charge_per_use or
                not def.requires_technic and def.custom_charge and (65535 - itemstack:get_wear()) < (65535 / uses)
                then
                    if def.reload_sound then
                        minetest.sound_play(def.reload_sound, {object=user, gain=reload_sound_gain, max_hear_distance=2*64})
                    end
                    return
                end

                if def.requires_technic then
                    meta.charge = meta.charge - technic_charge_per_use
                    technic.set_RE_wear(itemstack, meta.charge, technic_charge)
                    itemstack:set_metadata(minetest.serialize(meta))
                else
                    local wear = itemstack:get_wear()
                    wear = wear + (65535/uses)
                    if def.custom_charge and wear > 65535 then wear = 65535 end
                    itemstack:set_wear(wear)

                    if itemstack:get_count() == 0 then
                        minetest.sound_play("default_tool_breaks",
                            {pos = user:get_pos(), gain = 0.5}, true)
                    end
                end
            end

            if def.mana_per_use then
                mana.subtract(playername, def.mana_per_use)
            end

            local playerpos = user:get_pos()
            local dir = user:get_look_dir()
            local vel = {x = 0, y = 0, z = 0}
            if combine_velocity then
                vel = user:get_player_velocity()
            end

            -- Adjust the player's position if the player is mounted
            local _a, _b, mount_pos = user:get_attach()
            if mount_pos then
                -- Attachment positions are multiplied by 10 when calling
                -- set_attach, so make sure to account for that
                -- YAMS-TODO: the projectile spawn point is too high when on
                -- a horse or boat relative to the player's head
                playerpos.y = playerpos.y + (mount_pos.y / 10)
            end

            --Hitscan type, implemented in-place
            if hitscan then
                for i = 0,shot_amount-1,1 do
                    local ray = minetest.raycast({
                                                x=playerpos.x+dir.x*spawndist,
                                                y=playerpos.y+1.5+dir.y*spawndist,
                                                z=playerpos.z+dir.z*spawndist
                                                 },
                                                 {
                                                x=playerpos.x+(dir.x+bweapons.get_spread(spread))*distance,
                                                y=playerpos.y+1.5+(dir.y+bweapons.get_spread(spread))*distance,
                                                z=playerpos.z+(dir.z+bweapons.get_spread(spread))*distance
                                                 },
                                                 true, liquids_stop)
                    local j = 0
                    while j < penetration do
                        local target = ray:next()
                        local dropped = false
                        --Shoot everything except the user
                        if target and target.ref ~= user then
                            if aoe then
                                bweapons.damage_aoe(damage, user, target.intersection_point, aoe_radius)
                            else
                                if target.type == "object" then
                                    target.ref:punch(user, 1.0, {
                                    full_punch_interval=1.0,
                                    damage_groups={fleshy=damage},
                                    }, nil)
                                end
                            end
                            --Custom function called on every hit
                            if def.on_hit then
                                def.on_hit(user, target)
                            end
                            --Little hit flares
                            if def.hit_flare then
                                minetest.add_particle({
                                    pos = target.intersection_point,
                                    expirationtime = 0.2,
                                    size = hit_flare_size,
                                    collisiondetection = false,
                                    vertical = false,
                                    texture = def.hit_flare,
                                    glow = hit_flare_glow,
                            })
                            end
                            --Hit particles
                            if def.hit_particle then
                                minetest.add_particlespawner({
                                    amount = hit_particle_amount,
                                    time = 0.05,
                                    minpos = target.intersection_point,
                                    maxpos = target.intersection_point,
                                    minvel = {x=-hit_particle_velocity, y=-hit_particle_velocity, z=-hit_particle_velocity},
                                    maxvel = {x=hit_particle_velocity, y=hit_particle_velocity+1, z=hit_particle_velocity},
                                    minacc = {x=0, y=hit_particle_gravity, z=0},
                                    maxacc = {x=0, y=hit_particle_gravity, z=0},
                                    minexptime = 2,
                                    maxexptime = 4,
                                    minsize = hit_particle_min_size,
                                    maxsize = hit_particle_size,
                                    collisiondetection = false,
                                    collision_removal = false,
                                    object_collision = false,
                                    vertical = false,
                                    texture = def.hit_particle,
                                    glow = hit_particle_glow,
                            })
                            end
                            --Spawn a drop at the last stop of a hitscan if defined
                            if def.drop and j == penetration - 1 then
                                if math.random() < def.drop_chance then
                                    minetest.add_item(target.intersection_point, def.drop)
                                end
                            end
                            --Hit sound
                            if def.hit_sound then
                                minetest.sound_play(def.hit_sound, {pos=target.intersection_point, gain=hit_sound_gain, max_hear_distance=2*64})
                            end
                        --Don't shoot yourself in the legs and increase penetration count
                        elseif target and target.ref == user then
                            j = j - 1
                        end
                        --Spawn a particle trail that leads to the last surface
                        if def.trail_particle and j == penetration - 1 then
                            local trail_distance = 0
                            if target ~= nil then
                                local delta = {x = target.intersection_point.x - playerpos.x, y = target.intersection_point.y - playerpos.y, z = target.intersection_point.z - playerpos.z}
                                trail_distance = math.abs(delta.x) + math.abs(delta.y) + math.abs(delta.z)
                            else
                                trail_distance = distance
                            end
                            local trail_particles_per_node = math.floor(trail_distance/trail_particle_distance)
                            for z = 0, trail_particles_per_node - 1, 1 do
                                minetest.add_particlespawner({
                                amount = trail_particle_amount,
                                time = 0.05,
                                minpos = {
                                       x = (playerpos.x + dir.x * z * trail_particle_distance) - trail_particle_displacement,
                                       y = (playerpos.y + 1.5 + dir.y * z * trail_particle_distance) - trail_particle_displacement,
                                       z = (playerpos.z + dir.z * z * trail_particle_distance) - trail_particle_displacement
                                      },
                                maxpos = {
                                       x = (playerpos.x + dir.x * z * trail_particle_distance) + trail_particle_displacement,
                                       y = (playerpos.y + 1.5 + dir.y * z * trail_particle_distance) + trail_particle_displacement,
                                       z = (playerpos.z + dir.z * z * trail_particle_distance) + trail_particle_displacement
                                      },
                                minvel = {x=-trail_particle_velocity, y=-trail_particle_velocity, z=-trail_particle_velocity},
                                maxvel = {x=trail_particle_velocity, y=trail_particle_velocity, z=trail_particle_velocity},
                                minacc = {x=0, y=trail_particle_gravity, z=0},
                                maxacc = {x=0, y=trail_particle_gravity, z=0},
                                minexptime = 2,
                                maxexptime = 4,
                                minsize = trail_particle_min_size,
                                maxsize = trail_particle_size,
                                collisiondetection = false,
                                collision_removal = false,
                                object_collision = false,
                                vertical = false,
                                texture = def.trail_particle,
                                glow = trail_particle_glow,
                                })
                            end
                        end
                        j = j + 1
                    end
                end
            else
                --Projectile type, implemented in another function (see register_projectile())
                for i = 0,shot_amount-1,1
                do
                    local obj = minetest.add_entity({x=playerpos.x+dir.x*spawndist,
                                                     y=playerpos.y+1.5+dir.y*spawndist,
                                                     z=playerpos.z+dir.z*spawndist},
                                                    def.name.."_projectile")
                    if not obj then return end
                    --Set projectile owner
                    obj:get_luaentity().owner = user
                    --Set previous pos when just spawned
                    obj:get_luaentity().previous_pos = {x = playerpos.x, y = playerpos.y + 1, z = playerpos.z}
                    --Combined velocity with player velocity
                    obj:set_velocity({x=((dir.x+bweapons.get_spread(spread))*projectile_speed)+vel.x,
                                    y=((dir.y+bweapons.get_spread(spread))*projectile_speed)+vel.y,
                                    z=((dir.z+bweapons.get_spread(spread))*projectile_speed)+vel.z})
                    obj:set_acceleration({x=0, y=projectile_gravity, z=0})
                    obj:set_yaw(user:get_look_horizontal())
                end
            end

            --Fire flash
            if def.flare then
                minetest.add_particle({
                    pos = {x=playerpos.x+dir.x*spawndist, y=playerpos.y+1.4+dir.y*spawndist, z=playerpos.z+dir.z*spawndist},
                    expirationtime = 0.1,
                    size = flare_size,
                    collisiondetection = false,
                    vertical = false,
                    texture = def.flare,
                    glow = flare_glow,
                })
            end

            --Fire sound
            if def.fire_sound then minetest.sound_play(def.fire_sound, {object=user, gain=fire_sound_gain, max_hear_distance=2*64}) end

            --Add an item to the user inventory, if defined
            if def.return_item then
                if inv:room_for_item("main", {name = def.return_item}) then
                    inv:add_item("main", {name = def.return_item})
                else
                    local obj = minetest.add_item({x = playerpos.x, y = playerpos.y + 1.5, z = playerpos.z}, def.return_item)
                    obj:set_velocity({x = dir.x * 5, y = dir.y * 5, z = dir.z * 5})
                end
            end

            --Custom function executed on fire
            if def.on_fire then
                def.on_fire(itemstack, user, pointed_thing)
            end

            --Set reloading flag and timer
            players[playername] = {reloading=true}
            minetest.after(cooldown, reset_player_cooldown, user)

            return itemstack
        end,
    })

    --Register a crafting recipe
    if def.recipe then
        for _,v in pairs(def.recipe) do
            minetest.register_craft({
            type = craft_shape,
            output = def.name,
            recipe = v,
            replacements = def.replacements,
            })
        end
    end

    --Register tool as technic_powered
    if def.requires_technic then
        technic.register_power_tool(def.name, technic_charge)
    end

        --Register a new craft to repair the tool with it, if defined
    if def.repair_with then
        minetest.register_craft({
            type = "shapeless",
            output = def.name,
            recipe = {
                def.name,
                def.repair_with,
            },
        })

        minetest.register_on_craft(
            function(itemstack, player, old_craft_grid, craft_inv)
                local stack = bweapons_repair(itemstack, player, old_craft_grid, craft_inv, def.name, def.repair_with, uses, repair_uses)
                return stack
            end)

        minetest.register_craft_predict(
            function(itemstack, player, old_craft_grid, craft_inv)
                local stack = bweapons_repair(itemstack, player, old_craft_grid, craft_inv, def.name, def.repair_with, uses, repair_uses)
                return stack
            end)

    end

    -- Assign anvil difficulty at this point, if specified
    if def.anvil_repair ~= false and not def.repair_with and
            def.anvil_difficulty then
        minetest.override_item(def.name, {
            _yams_anvil_difficulty = def.anvil_difficulty,
        })
    end
end

--Main ammo registration function
function bweapons.register_ammo(def)

    local amount = def.amount or 1

    minetest.register_craftitem(def.name,{
        description = def.description,
        inventory_image = def.texture,
        wield_image = def.texture,
        _tt_help = def._tt_help,
    })

    if def.recipe then
        for _,v in pairs(def.recipe) do
            minetest.register_craft({
            type = "shaped",
            output = def.name .. " " .. amount,
            recipe = v,
            })
        end
    end

end
