--[[ Shooter API [shooter] Copyright (C) 2013-2019 stujones11, Stuart Jones This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ]]-- shooter = { registered_weapons = {}, } shooter.config = { automatic_weapons = true, admin_weapons = false, enable_blasting = false, enable_particle_fx = true, enable_protection = false, enable_crafting = true, explosion_texture = "shooter_hit.png", allow_nodes = true, allow_entities = false, allow_players = true, rounds_update_time = 0.4, camera_height = 1.5, } shooter.default_particles = { amount = 15, time = 0.3, minpos = {x=-0.1, y=-0.1, z=-0.1}, maxpos = {x=0.1, y=0.1, z=0.1}, minvel = {x=-1, y=1, z=-1}, maxvel = {x=1, y=2, z=1}, minacc = {x=-2, y=-2, z=-2}, maxacc = {x=2, y=-2, z=2}, minexptime = 0.1, maxexptime = 0.75, minsize = 1, maxsize = 2, collisiondetection = false, texture = "shooter_hit.png", } local shots = {} local shooting = {} local config = shooter.config local server_step = minetest.settings:get("dedicated_server_step") shooter.register_weapon = function(name, def) shooter.registered_weapons[name] = def -- Fix definition table def.sounds = def.sounds or {} def.sounds.reload = def.sounds.reload or "shooter_reload" def.sounds.fail_shot = def.sounds.fail_shot or "shooter_click" def.reload_item = def.reload_item or "shooter:ammo" def.spec.tool_caps.full_punch_interval = math.max(server_step, def.spec.tool_caps.full_punch_interval) def.spec.wear = math.ceil(65535 / def.spec.rounds) def.spec.unloaded_item = name def.unloaded_item = def.unloaded_item or { description = def.description.." (unloaded)", inventory_image = def.inventory_image, } -- Register loaded item tool minetest.register_tool(name.."_loaded", { description = def.description, inventory_image = def.inventory_image, on_use = function(itemstack, user, pointed_thing) if type(def.on_use) == "function" then itemstack = def.on_use(itemstack, user, pointed_thing) end local spec = table.copy(def.spec) if shooter.fire_weapon(user, itemstack, spec) then itemstack:add_wear(def.spec.wear) if itemstack:get_count() == 0 then itemstack:replace(def.unloaded_item.name) end end return itemstack end, on_hit = def.on_hit, groups = {not_in_creative_inventory=1}, }) -- Register unloaded item tool minetest.register_tool(name, { description = def.unloaded_item.description, inventory_image = def.unloaded_item.inventory_image, groups = def.unloaded_item.groups or {}, on_use = function(itemstack, user) local inv = user:get_inventory() if inv then local stack = def.reload_item if inv:contains_item("main", stack) then minetest.sound_play(def.sounds.reload, {object=user}) inv:remove_item("main", stack) itemstack:replace(name.."_loaded 1 1") else minetest.sound_play(def.sounds.fail_shot, {object=user}) end end return itemstack end, }) end shooter.get_configuration = function(conf) for k, v in pairs(conf) do local setting = minetest.settings:get("shooter_"..k) if type(v) == "number" then setting = tonumber(setting) elseif type(v) == "boolean" then setting = minetest.settings:get_bool("shooter_"..k) end if setting ~= nil then conf[k] = setting end end return conf end shooter.spawn_particles = function(pos, particles) particles = particles or {} if not config.enable_particle_fx == true or particles.amount == 0 then return end local copy = function(v) return type(v) == "table" and table.copy(v) or v end local p = {} for k, v in pairs(shooter.default_particles) do p[k] = particles[k] and copy(particles[k]) or copy(v) end p.minpos = vector.subtract(pos, p.minpos) p.maxpos = vector.add(pos, p.maxpos) minetest.add_particlespawner(p) end shooter.play_node_sound = function(node, pos) local item = minetest.registered_items[node.name] if item then if item.sounds then local spec = item.sounds.dug if spec then spec.pos = pos minetest.sound_play(spec.name, spec) end end end end shooter.punch_node = function(pos, spec) local node = minetest.get_node(pos) if not node then return end local item = minetest.registered_items[node.name] if not item then return end if config.enable_protection then if minetest.is_protected(pos, spec.user) then return end end if item.groups then for k, v in pairs(spec.groups) do local level = item.groups[k] or 0 if level >= v then minetest.remove_node(pos) shooter.play_node_sound(node, pos) if item.tiles then if item.tiles[1] then shooter.spawn_particles(pos, {texture=item.tiles[1]}) end end break end end end end shooter.is_valid_object = function(object) if object then if object:is_player() == true then return config.allow_players end if config.allow_entities == true then local luaentity = object:get_luaentity() if luaentity then return luaentity.name ~= "__builtin:item" end end end end local function process_hit(pointed_thing, spec, dir) if pointed_thing.type == "node" and config.allow_nodes == true then local pos = minetest.get_pointed_thing_position(pointed_thing, false) shooter.punch_node(pos, spec) elseif pointed_thing.type == "object" then local object = pointed_thing.ref if shooter.is_valid_object(object) == true then local player = minetest.get_player_by_name(spec.user) if player then object:punch(player, nil, spec.tool_caps, dir) local pos = pointed_thing.intersection_point or object:get_pos() local groups = object:get_armor_groups() or {} if groups.fleshy then shooter.spawn_particles(pos, spec.particles) end end end end local def = minetest.registered_items[spec.name] or {} if type(def.on_hit) == "function" then return def.on_hit(pointed_thing, spec, dir) end end local function process_round(round) round.dist = round.dist + round.spec.step if round.dist > round.spec.range then return end local p1 = round.pos local p2 = vector.add(p1, vector.multiply(round.dir, round.spec.step)) local ray = minetest.raycast(p1, p2, true, true) local pointed_thing = ray:next() or {type="nothing"} if pointed_thing.type ~= "nothing" then return process_hit(pointed_thing, round.spec, round.dir) end round.pos = p2 minetest.after(shooter.config.rounds_update_time, function(...) process_round(...) end, round) end local function fire_weapon(player, itemstack, spec, extended) if not player then return end local dir = player:get_look_dir() local pos = player:get_pos() if not dir or not pos then return end pos.y = pos.y + config.camera_height minetest.sound_play(spec.sound, {object=player}) if spec.bullet_image then minetest.add_particle({ pos = pos, velocity = vector.multiply(dir, 30), acceleration = {x=0, y=0, z=0}, expirationtime = 0.5, size = 0.25, texture = spec.bullet_image, }) end process_round({ spec = spec, pos = vector.add(pos, dir), dir = dir, dist = 0, }) if extended then itemstack:add_wear(spec.wear) if itemstack:get_count() == 0 then itemstack = spec.unloaded_item player:set_wielded_item(itemstack) return end player:set_wielded_item(itemstack) end if not spec.automatic or not shooting[spec.user] then return end local interval = spec.tool_caps.full_punch_interval shots[spec.user] = minetest.get_us_time() / 1000000 + interval minetest.after(interval, function(...) if shooting[spec.user] then local arg = {...} fire_weapon(arg[1], arg[2], arg[3], true) end end, player, itemstack, spec) end shooter.fire_weapon = function(player, itemstack, spec) local name = player:get_player_name() local time = minetest.get_us_time() / 1000000 if shots[name] and time <= shots[name] then return false end if config.automatic_weapons then if config.admin_weapons and minetest.check_player_privs(name, {server=true}) then spec.automatic = true end shooting[name] = true end spec.user = name spec.name = itemstack:get_name() fire_weapon(player, itemstack, spec) return true end shooter.blast = function(pos, radius, fleshy, distance, user) if not user then return end pos = vector.round(pos) local name = user:get_player_name() local p1 = vector.subtract(pos, radius) local p2 = vector.add(pos, radius) minetest.sound_play("tnt_explode", {pos=pos, gain=1}) if config.allow_nodes and config.enable_blasting then if config.enable_protection then if not minetest.is_protected(pos, name) then minetest.set_node(pos, {name="tnt:boom"}) end else minetest.set_node(pos, {name="tnt:boom"}) end end if config.enable_particle_fx == true then minetest.add_particlespawner({ amount = 50, time = 0.1, minpos = p1, maxpos = p2, minvel = {x=0, y=0, z=0}, maxvel = {x=0, y=0, z=0}, minacc = {x=-0.5, y=5, z=-0.5}, maxacc = {x=0.5, y=5, z=0.5}, minexptime = 0.1, maxexptime = 1, minsize = 8, maxsize = 15, collisiondetection = false, texture = "tnt_smoke.png", }) end local objects = minetest.get_objects_inside_radius(pos, distance) for _,obj in ipairs(objects) do if shooter.is_valid_object(obj) then local obj_pos = obj:get_pos() local dist = vector.distance(obj_pos, pos) local damage = (fleshy * 0.5 ^ dist) * 2 if dist ~= 0 then obj_pos.y = obj_pos.y + 1 local blast_pos = {x=pos.x, y=pos.y + 4, z=pos.z} if minetest.line_of_sight(obj_pos, blast_pos, 1) then obj:punch(user, 1.0, { full_punch_interval = 1.0, damage_groups = {fleshy=damage}, }) end end end end if config.allow_nodes and config.enable_blasting then local pr = PseudoRandom(os.time()) local vm = VoxelManip() local min, max = vm:read_from_map(p1, p2) local area = VoxelArea:new({MinEdge=min, MaxEdge=max}) local data = vm:get_data() local c_air = minetest.get_content_id("air") for z = -radius, radius do for y = -radius, radius do local vp = {x=pos.x - radius, y=pos.y + y, z=pos.z + z} local vi = area:index(vp.x, vp.y, vp.z) for x = -radius, radius do if (x * x) + (y * y) + (z * z) <= (radius * radius) + pr:next(-radius, radius) then if config.enable_protection then if not minetest.is_protected(vp, name) then data[vi] = c_air end else data[vi] = c_air end end vi = vi + 1 end end end vm:set_data(data) vm:update_liquids() vm:write_to_map() end end shooter.get_shooting = function(name) return shooting[name] end shooter.set_shooting = function(name, is_shooting) shooting[name] = is_shooting and true or nil end