575 lines
16 KiB
Lua
575 lines
16 KiB
Lua
--[[
|
|
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 = true,
|
|
enable_crafting = true,
|
|
explosion_texture = "shooter_hit.png",
|
|
node_drops = false,
|
|
allow_nodes = true,
|
|
allow_entities = false,
|
|
allow_players = true,
|
|
rounds_update_time = 0.4,
|
|
damage_multiplier = 1,
|
|
}
|
|
|
|
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 = table.copy(shooter.config)
|
|
local server_step = minetest.settings:get("dedicated_server_step")
|
|
local v3d = table.copy(vector)
|
|
local PI = math.pi
|
|
local sin = math.sin
|
|
local cos = math.cos
|
|
local sqrt = math.sqrt
|
|
local phi = (math.sqrt(5) + 1) / 2 -- Golden ratio
|
|
|
|
shooter.register_weapon = function(name, def)
|
|
-- Backwards compatibility
|
|
if not def.spec.sounds then
|
|
def.spec.sounds = def.sounds or {}
|
|
end
|
|
if not def.spec.sounds.shot and def.spec.sound then
|
|
def.spec.sounds.shot = def.spec.sound
|
|
end
|
|
-- Fix definition table
|
|
def.spec.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.unloaded_item = def.unloaded_item or {
|
|
description = def.description.." (unloaded)",
|
|
inventory_image = def.inventory_image,
|
|
}
|
|
def.unloaded_item.name = name
|
|
shooter.registered_weapons[name] = table.copy(def)
|
|
-- 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
|
|
if itemstack then
|
|
local spec = shooter.get_weapon_spec(user, name)
|
|
if spec and 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
|
|
end
|
|
return itemstack
|
|
end,
|
|
unloaded_item = def.unloaded_item,
|
|
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.spec.reload_item
|
|
if inv:contains_item("main", stack) then
|
|
local sound = def.spec.sounds.reload or "shooter_reload"
|
|
minetest.sound_play(sound, {object=user})
|
|
inv:remove_item("main", stack)
|
|
itemstack:replace(name.."_loaded 1 1")
|
|
else
|
|
local sound = def.spec.sounds.fail_shot or "shooter_click"
|
|
minetest.sound_play(sound, {object=user})
|
|
end
|
|
end
|
|
return itemstack
|
|
end,
|
|
})
|
|
end
|
|
|
|
shooter.get_weapon_spec = function(_, name)
|
|
local def = shooter.registered_weapons[name]
|
|
if def then
|
|
return table.copy(def.spec)
|
|
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
|
|
for k, v in pairs(shooter.default_particles) do
|
|
if not particles[k] then
|
|
particles[k] = type(v) == "table" and table.copy(v) or v
|
|
end
|
|
end
|
|
particles.minpos = v3d.subtract(pos, particles.minpos)
|
|
particles.maxpos = v3d.add(pos, particles.maxpos)
|
|
minetest.add_particlespawner(particles)
|
|
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.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
|
|
|
|
shooter.punch_node = function(pos, spec)
|
|
if config.enable_protection and minetest.is_protected(pos, spec.user, {is_gun = true}) then
|
|
return
|
|
end
|
|
|
|
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 node.name:find("ctf_traps") and item.on_dig then
|
|
item.on_dig(pos, node, minetest.get_player_by_name(spec.user), {do_dig = false})
|
|
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.dig_node(pos)
|
|
shooter.play_node_sound(node, pos)
|
|
if item.tiles then
|
|
local texture = item.tiles[1]
|
|
texture = (type(texture) == "table") and texture.name or texture
|
|
shooter.spawn_particles(pos, {texture=texture})
|
|
end
|
|
if config.node_drops then
|
|
local object = minetest.add_item(pos, item)
|
|
if object then
|
|
object:set_velocity({
|
|
x = math.random(-1, 1),
|
|
y = 4,
|
|
z = math.random(-1, 1)
|
|
})
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
shooter.punch_object = function(object, tool_caps, dir, on_blast, puncher)
|
|
if type(puncher) == "string" then
|
|
puncher = minetest.get_player_by_name(puncher)
|
|
end
|
|
|
|
local do_damage = true
|
|
local groups = tool_caps.damage_groups or {}
|
|
if on_blast and not object:is_player() then
|
|
local ent = object:get_luaentity()
|
|
if ent then
|
|
local def = minetest.registered_entities[ent.name] or {}
|
|
if def.on_blast and groups.fleshy then
|
|
do_damage = def.on_blast(ent, groups.fleshy *
|
|
config.damage_multiplier)
|
|
end
|
|
end
|
|
end
|
|
if do_damage then
|
|
for k, v in pairs(groups) do
|
|
tool_caps.damage_groups[k] = v * config.damage_multiplier
|
|
end
|
|
object:punch(puncher, nil, tool_caps, dir)
|
|
return true
|
|
end
|
|
end
|
|
|
|
local function matrix_from_quat(q)
|
|
local m = {{}, {}, {}}
|
|
m[1][1] = 1 - 2 * q.y * q.y - 2 * q.z * q.z
|
|
m[1][2] = 2 * q.x * q.y + 2 * q.z * q.w
|
|
m[1][3] = 2 * q.x * q.z - 2 * q.y * q.w
|
|
m[2][1] = 2 * q.x * q.y - 2 * q.z * q.w
|
|
m[2][2] = 1 - 2 * q.x * q.x - 2 * q.z * q.z
|
|
m[2][3] = 2 * q.z * q.y + 2 * q.x * q.w
|
|
m[3][1] = 2 * q.x * q.z + 2 * q.y * q.w
|
|
m[3][2] = 2 * q.z * q.y - 2 * q.x * q.w
|
|
m[3][3] = 1 - 2 * q.x * q.x - 2 * q.y * q.y
|
|
return m
|
|
end
|
|
|
|
local function quat_from_angle_axis(angle, axis)
|
|
local t = angle / 2
|
|
local s = sin(t)
|
|
return {
|
|
x = s * axis.x,
|
|
y = s * axis.y,
|
|
z = s * axis.z,
|
|
w = cos(t),
|
|
}
|
|
end
|
|
|
|
v3d.cross = function(v1, v2)
|
|
return {
|
|
x = v1.y * v2.z - v2.y * v1.z,
|
|
y = v1.z * v2.x - v2.z * v1.x,
|
|
z = v1.x * v2.y - v2.x * v1.y,
|
|
}
|
|
end
|
|
|
|
v3d.mult_matrix = function(v, m)
|
|
return {
|
|
x = m[1][1] * v.x + m[1][2] * v.y + m[1][3] * v.z,
|
|
y = m[2][1] * v.x + m[2][2] * v.y + m[2][3] * v.z,
|
|
z = m[3][1] * v.x + m[3][2] * v.y + m[3][3] * v.z,
|
|
}
|
|
end
|
|
|
|
v3d.rotate = function(v, angle, axis)
|
|
local q = quat_from_angle_axis(angle, axis)
|
|
local m = matrix_from_quat(q)
|
|
return v3d.mult_matrix(v, m)
|
|
end
|
|
|
|
local function get_directions(dir, spec)
|
|
local directions = {dir}
|
|
local n = spec.shots or 1
|
|
if n > 1 then
|
|
local right = v3d.normalize(v3d.cross(dir, {x=0, y=1, z=0}))
|
|
local up = v3d.normalize(v3d.cross(dir, right))
|
|
local s = spec.spread or 10
|
|
s = s * 0.017453 -- Convert to radians
|
|
for k = 1, n - 1 do
|
|
-- Sunflower seed arrangement
|
|
local r = sqrt(k - 0.5) / sqrt(n - 0.5)
|
|
local theta = 2 * PI * k / (phi * phi)
|
|
local x = r * cos(theta) * s
|
|
local y = r * sin(theta) * s
|
|
local d = v3d.rotate(dir, y, up)
|
|
directions[k + 1] = v3d.rotate(d, x, right)
|
|
end
|
|
end
|
|
return directions
|
|
end
|
|
|
|
local function process_hit(pointed_thing, spec, dir)
|
|
local def = minetest.registered_items[spec.name] or {}
|
|
if type(def.on_hit) == "function" then
|
|
if def.on_hit(pointed_thing, spec, dir) == true then
|
|
return
|
|
end
|
|
end
|
|
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) and
|
|
shooter.punch_object(object, spec.tool_caps, dir, nil, spec.user) then
|
|
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 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 = v3d.add(p1, v3d.multiply(round.dir, round.spec.step))
|
|
local ray = minetest.raycast(p1, p2, true, true)
|
|
local pointed_thing = ray:next()
|
|
if pointed_thing then
|
|
-- Iterate over ray again if pointed object == shooter
|
|
local ref = pointed_thing.ref
|
|
if ref and ref:is_player() and ref:get_player_name() == round.spec.user then
|
|
pointed_thing = ray:next()
|
|
end
|
|
|
|
if pointed_thing then
|
|
return process_hit(pointed_thing, round.spec, round.dir)
|
|
end
|
|
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
|
|
|
|
if spec.user_knockback then
|
|
if vector.distance(player:get_velocity(), vector.new()) < 10 then
|
|
local vel = vector.multiply(minetest.yaw_to_dir(player:get_look_horizontal()), -spec.user_knockback)
|
|
if vel.y > 0 then
|
|
vel.y = spec.user_knockback/5
|
|
end
|
|
player:add_player_velocity(vel)
|
|
end
|
|
end
|
|
|
|
pos.y = pos.y + player:get_properties().eye_height
|
|
spec.origin = pos
|
|
local interval = spec.tool_caps.full_punch_interval
|
|
shots[spec.user] = minetest.get_us_time() / 1000000 + interval
|
|
local sound = spec.sounds.shot or "shooter_pistol"
|
|
minetest.sound_play(sound, {object=player})
|
|
local speed = spec.step / (config.rounds_update_time * 2)
|
|
local time = spec.range / speed
|
|
local directions = get_directions(dir, spec)
|
|
for _, d in pairs(directions) do
|
|
if spec.bullet_image then
|
|
minetest.add_particle({
|
|
pos = pos,
|
|
velocity = v3d.multiply(d, speed),
|
|
acceleration = {x=0, y=0, z=0},
|
|
expirationtime = time,
|
|
size = 0.25,
|
|
texture = spec.bullet_image,
|
|
})
|
|
end
|
|
process_round({
|
|
spec = spec,
|
|
pos = v3d.new(spec.origin),
|
|
dir = d,
|
|
dist = 0,
|
|
})
|
|
end
|
|
if extended then
|
|
itemstack:add_wear(spec.wear)
|
|
if itemstack:get_count() == 0 then
|
|
local def = minetest.registered_items[spec.name] or {}
|
|
if def.unloaded_item then
|
|
itemstack = def.unloaded_item.name or ""
|
|
end
|
|
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
|
|
minetest.after(interval, function(...)
|
|
if shooting[spec.user] and player:get_wield_index() == spec.wield_idx 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()
|
|
spec.wield_idx = player:get_wield_index()
|
|
fire_weapon(player, itemstack, spec)
|
|
return true
|
|
end
|
|
|
|
shooter.blast = function(pos, radius, fleshy, distance, user)
|
|
if not user then
|
|
return
|
|
end
|
|
pos = v3d.round(pos)
|
|
local name = user:get_player_name()
|
|
local p1 = v3d.subtract(pos, radius)
|
|
local p2 = v3d.add(pos, radius)
|
|
minetest.sound_play("shooter_explode", {
|
|
pos = pos,
|
|
gain = 10,
|
|
max_hear_distance = 100
|
|
})
|
|
if config.allow_nodes and config.enable_blasting then
|
|
if not config.enable_protection or
|
|
not minetest.is_protected(pos, name) then
|
|
minetest.set_node(pos, {name="shooter: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 = "shooter_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 = v3d.distance(obj_pos, pos)
|
|
local damage = (fleshy * 0.5 ^ dist) * 2 * config.damage_multiplier
|
|
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 shooter.is_valid_object(obj) and
|
|
minetest.line_of_sight(obj_pos, blast_pos, 1) then
|
|
shooter.punch_object(obj, {
|
|
full_punch_interval = 1.0,
|
|
damage_groups = {fleshy=damage},
|
|
}, nil, true, user)
|
|
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
|
|
|
|
minetest.register_on_dieplayer(function(player)
|
|
shooting[player:get_player_name()] = nil
|
|
end)
|