projectile/init.lua

616 lines
21 KiB
Lua

--[[
Research N' Duplication
Copyright (C) 2020 Noodlemire
This library 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 library 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 library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
--]]
--Mod-specific global variable
projectile = {}
--A list of registered projectile entites, indexed by the category of weapon that uses them.
projectile.registered_projectiles = {}
--Per-player list of much a projectile weapon has been charged.
projectile.charge_levels = {}
--MP = Mod Path
local mp = minetest.get_modpath(minetest.get_current_modname())..'/'
--In here is every publicly available function that this mod uses.
dofile(mp.."api.lua")
--In here is the registration of ammo items that this mod provides, as well as crafting recipes for weapons and ammo.
dofile(mp.."crafts.lua")
--A helper function to cancel a player's charge when necessary.
local function uncharge_player(player)
--Useful shorthand
local pname = player:get_player_name()
--If there is charge data...
if projectile.charge_levels[pname] then
--Get store the previous slot for possible use after charge data deletion.
local old_slot = projectile.charge_levels[pname].slot
--Get the projectile weapon. get_wielded_item() can't be used, since the weapon may no longer be held.
local wep = player:get_inventory():get_stack("main", old_slot)
--If the weapon was /pulverized or otherwise deleted without triggering a callback...
if wep:is_empty() then
--Delete the entry directly, which normally happens inside the weapon's on_use function.
projectile.charge_levels[pname] = nil
else
--Call the weapon's on_use function, which will cancel it.
wep:get_definition().on_use(wep, player, true)
end
--Update the player's inventory with any modifications.
player:get_inventory():set_stack("main", old_slot, wep)
end
end
--Globalsteps are used to either cancel a charge if a player switches weapons, or to update the weapon sprite when charging is complete.
minetest.register_globalstep(function(dtime)
--For each player on the server...
for _, player in pairs(minetest.get_connected_players()) do
--Useful shorthand
local pname = player:get_player_name()
--If this player is currently charging a projectile weapon...
if projectile.charge_levels[pname] then
--If the player's selected hotbar slot changed or their weapon was deleted somehow...
if player:get_wield_index() ~= projectile.charge_levels[pname].slot or player:get_wielded_item():is_empty() then
--Cancel their charge.
uncharge_player(player)
--Otherwise, as long as the player doesn't change weapon...
else
--Add a little charge, according the how much time that has passed since the last globalstep.
projectile.charge_levels[pname].charge = projectile.charge_levels[pname].charge + dtime
--Get the charging weapon and its definition
local wep = player:get_wielded_item()
local def = wep:get_definition()
--if the weapon has a listed full_charge_name, meaning it hasn't already fully charged,
--but now the charge level has reached or exceeded the max...
if def.full_charge_name and projectile.charge_levels[pname].charge >= def.charge_time then
--Once this happens, replace the weapon with a fully charged sprite version.
wep:set_name(def.full_charge_name)
player:set_wielded_item(wep)
--Enable a callback that can occur when the weapon is fully charged
if def.on_charge_full then
def.on_charge_full(wep, player)
end
end
end
end
end
end)
--When a weapon is charging, it's a lot harder to check for when the stack has moved elsewhere, at least in terms of checking it.
--So, if a player tries any inventory action related to a charging projectile weapon, prevent it.
minetest.register_allow_player_inventory_action(function(player, action, inv, info)
if (action == "take" and minetest.get_item_group(info.stack:get_name(), "projectile_weapon") >= 2) or
(action == "put" and minetest.get_item_group(inv:get_stack(info.listname, info.index):get_name(), "projectile_weapon") >= 2) or
(action == "move" and
(minetest.get_item_group(inv:get_stack(info.from_list, info.from_index):get_name(), "projectile_weapon") >= 2 or
minetest.get_item_group(inv:get_stack(info.to_list, info.to_index):get_name(), "projectile_weapon") >= 2)) then
return 0
end
end)
--If a player leaves, cancel their charge.
minetest.register_on_leaveplayer(function(player)
uncharge_player(player)
end)
--If a server is shutdown, cancel all charges.
minetest.register_on_shutdown(function()
for _, player in pairs(minetest.get_connected_players()) do
uncharge_player(player)
end
end)
--The basic slingshot. Slingshots are weaker than bows, but the ammunition they use is way easier to find and create.
projectile.register_weapon("projectile:slingshot", {
description = "Slingshot",
inventory_image = "projectile_slingshot.png",
inventory_image_2 = "projectile_slingshot_charged.png",
inventory_image_3 = "projectile_slingshot_charged_full.png",
durability = 75,
rw_category = "slingshot",
charge = true,
fire_while_charging = true,
on_charge_begin = function(wep, user)
projectile.charge_levels[user:get_player_name()].sound = minetest.sound_play("projectile_slingshot_drawn_adam-n", {gain = 1.0, object = user})
end,
after_fire = function(wep, user)
minetest.sound_play("projectile_bow_release_porkmuncher", {gain = 1.0, pos = user:get_pos()}, true)
end
})
--An upgraded slingshot, which fires faster and harder, but is slightly harder to charge up. Metal wire is stiffer than string, after all.
projectile.register_weapon("projectile:steel_slingshot", {
description = "Steel Slingshot",
inventory_image = "projectile_steel_slingshot.png",
inventory_image_2 = "projectile_steel_slingshot_charged.png",
inventory_image_3 = "projectile_steel_slingshot_charged_full.png",
durability = 150,
rw_category = "slingshot",
charge = true,
fire_while_charging = true,
charge_time = 1.1,
damage = 1.25,
speed = 1.75,
on_charge_begin = function(wep, user)
projectile.charge_levels[user:get_player_name()].sound = minetest.sound_play("projectile_slingshot_drawn_adam-n", {gain = 1.0, object = user})
end,
after_fire = function(wep, user)
minetest.sound_play("projectile_bow_release_porkmuncher", {gain = 1.0, pos = user:get_pos()}, true)
end
})
--The basic bow. It is more powerful than a slingshot, but it takes way longer to charge and ammunition is harder to get.
projectile.register_weapon("projectile:bow", {
description = "Bow",
inventory_image = "projectile_bow.png",
inventory_image_2 = "projectile_bow_charged.png",
inventory_image_3 = "projectile_bow_charged_full.png",
durability = 100,
rw_category = "bow",
charge = true,
fire_while_charging = true,
charge_time = 2,
on_charge_begin = function(wep, user)
projectile.charge_levels.sound = minetest.sound_play("projectile_bow_drawn_paveroux", {gain = 1.0, object = user})
end,
after_fire = function(wep, user)
minetest.sound_play("projectile_bow_release_porkmuncher", {gain = 1.0, pos = user:get_pos()}, true)
end
})
--An upgraded bow, which fires faster and harder, but is slightly harder to charge up. Metal wire is stiffer than string, after all.
projectile.register_weapon("projectile:steel_bow", {
description = "Steel Bow",
inventory_image = "projectile_steel_bow.png",
inventory_image_2 = "projectile_steel_bow_charged.png",
inventory_image_3 = "projectile_steel_bow_charged_full.png",
durability = 200,
rw_category = "bow",
charge = true,
fire_while_charging = true,
charge_time = 2.1,
damage = 1.5,
speed = 1.9,
on_charge_begin = function(wep, user)
projectile.charge_levels[user:get_player_name()].sound = minetest.sound_play("projectile_bow_drawn_paveroux", {gain = 1.0, object = user})
end,
after_fire = function(wep, user)
minetest.sound_play("projectile_bow_release_porkmuncher", {gain = 1.0, pos = user:get_pos()}, true)
end
})
--The basic flintlock weapon, which can fire fairly often and has fair damage, but can't be fired before it is fully loaded.
projectile.register_weapon("projectile:flintlock_pistol", {
description = "Flintlock Pistol",
inventory_image = "projectile_flintlock_pistol.png",
inventory_image_2 = "projectile_flintlock_pistol.png",
inventory_image_3 = "projectile_flintlock_pistol_charged.png",
durability = 250,
rw_category = "flintlock",
charge = true,
charge_time = 0.667,
can_fire = projectile.needs_gunpowder,
on_cancel = projectile.return_gunpowder,
on_charge_begin = function(wep, user)
minetest.sound_play("projectile_metal_click_mkoenig", {gain = 1.0, pos = user:get_pos()}, true)
end,
on_charge_full = function(wep, user)
minetest.sound_play("projectile_metal_click_mkoenig", {gain = 1.0, pos = user:get_pos()}, true)
end,
after_fire = function(wep, user)
minetest.sound_play("projectile_musket_shot_aaronsiler", {gain = 0.75, pos = user:get_pos(), max_hear_distance = 48}, true)
end,
})
--A slowler, more powerful flintlock weapon.
projectile.register_weapon("projectile:musket", {
description = "Musket",
inventory_image = "projectile_musket.png",
inventory_image_2 = "projectile_musket.png",
inventory_image_3 = "projectile_musket_charged.png",
durability = 300,
rw_category = "flintlock",
charge = true,
charge_time = 1.333,
damage = 1.5,
speed = 1.1,
can_fire = projectile.needs_gunpowder,
on_cancel = projectile.return_gunpowder,
on_charge_begin = function(wep, user)
minetest.sound_play("projectile_metal_click_mkoenig", {gain = 1.0, pos = user:get_pos()}, true)
end,
on_charge_full = function(wep, user)
minetest.sound_play("projectile_metal_click_mkoenig", {gain = 1.0, pos = user:get_pos()}, true)
end,
after_fire = function(wep, user)
minetest.sound_play("projectile_musket_shot_aaronsiler", {gain = 1.0, pos = user:get_pos(), max_hear_distance = 96}, true)
end,
})
--A flintlock weapon that fires bursts of shot, rather than individual musket balls.
projectile.register_weapon("projectile:blunderbuss", {
description = "Blunderbuss",
inventory_image = "projectile_blunderbuss.png",
inventory_image_2 = "projectile_blunderbuss.png",
inventory_image_3 = "projectile_blunderbuss_charged.png",
durability = 250,
rw_category = "flintlock_shot",
charge = true,
charge_time = 1,
can_fire = projectile.needs_gunpowder,
on_cancel = projectile.return_gunpowder,
on_charge_begin = function(wep, user)
minetest.sound_play("projectile_metal_click_mkoenig", {gain = 1.0, pos = user:get_pos()}, true)
end,
on_charge_full = function(wep, user)
minetest.sound_play("projectile_metal_click_mkoenig", {gain = 1.0, pos = user:get_pos()}, true)
end,
after_fire = function(wep, user)
minetest.sound_play("projectile_musket_shot_aaronsiler", {gain = 1.0, pos = user:get_pos(), max_hear_distance = 96}, true)
end,
})
--The basic slingshot projectile: rocks, optionally the ones from hardtrees
if hardtrees then
projectile.register_projectile("projectile:rock", "slingshot", "hardtrees:rock", {
image = "rock_lump.png",
damage = 5,
speed = 15
})
else
projectile.register_projectile("projectile:rock", "slingshot", "projectile:rock", {
image = "projectile_rock.png",
damage = 5,
speed = 15
})
end
--A helper function for mese projectiles, to check if a particular node can be powered.
local is_mesecon = function(pos)
local def = minetest.registered_nodes[minetest.get_node(pos).name]
return def and def.mesecons
end
--Mese projectiles for slingshots, which have medium power and can be used to power mesecon effectors.
projectile.register_projectile("projectile:mese", "slingshot", "default:mese_crystal_fragment", {
image = "default_mese_crystal_fragment.png",
damage = 7,
speed = 20,
--When a mese crystal fragment hits something...
on_impact = function(self, collisions)
--If the mesecon mod is loaded...
if mesecon then
--For each collided thing...
for _, c in pairs(collisions) do
--If the thing is a node and can be powered...
if c.type == "node" and is_mesecon(c.node_pos) then
--Grab data about the node.
local node = minetest.get_node(c.node_pos)
--As long as it isn't already powered...
if not mesecon.is_powered(c.node_pos) then
--Activate the node.
mesecon.activate(c.node_pos, node, nil, 0)
--Then, after 1/4 of the second, deactivate it.
minetest.after(0.25, function() mesecon.deactivate(c.node_pos, node, nil, 0) end)
end
end
end
end
end
})
--Obsidian shards are the strongest slingshot projectile.
projectile.register_projectile("projectile:obsidian", "slingshot", "default:obsidian_shard", {
image = "default_obsidian_shard.png",
damage = 9,
speed = 25
})
--The basic arrow, which has twice the power of a rock.
projectile.register_projectile("projectile:arrow", "bow", "projectile:arrow", {
damage = 10,
speed = 30,
initial_properties = {
visual = "mesh",
mesh = "projectile_arrow.obj",
textures = {"projectile_arrow_texture.png"}
},
_on_step = function(self, dtime)
projectile.autorotate_arrow(self, dtime)
if fire then
local selfo = self.object
local node = minetest.get_node(selfo:get_pos())
if minetest.get_item_group(node.name, "lava") > 0 or minetest.get_item_group(node.name, "fire") > 0 then
local arrow = minetest.add_entity(selfo:get_pos(), "projectile:arrow_fire")
local arrowlua = arrow:get_luaentity()
arrow:set_velocity(selfo:get_velocity())
arrow:set_acceleration(selfo:get_acceleration())
arrow:set_rotation(selfo:get_rotation())
arrowlua.level = self.level
arrowlua.damage = 12
arrowlua.owner = self.owner
arrowlua.oldvel = self.oldvel
arrowlua.timer = self.timer
selfo:remove()
end
end
end
})
--If the fire mod is present...
if fire then
--Register arrows that can combust flammable terrain.
projectile.register_projectile("projectile:arrow_fire", "bow", "projectile:arrow_fire", {
damage = 12,
speed = 35,
initial_properties = {
visual = "mesh",
mesh = "projectile_arrow.obj",
textures = {"projectile_arrow_fire_texture.png"}
},
_on_step = function(self, dtime)
projectile.autorotate_arrow(self, dtime)
local selfo = self.object
local node = minetest.get_node(selfo:get_pos())
if minetest.get_item_group(node.name, "water") > 0 then
local arrow = minetest.add_entity(selfo:get_pos(), "projectile:arrow")
local arrowlua = arrow:get_luaentity()
arrow:set_velocity(selfo:get_velocity())
arrow:set_acceleration(selfo:get_acceleration())
arrow:set_rotation(selfo:get_rotation())
arrowlua.level = self.level
arrowlua.damage = 10
arrowlua.owner = self.owner
arrowlua.oldvel = self.oldvel
arrowlua.timer = self.timer
selfo:remove()
end
end,
--On impact...
on_impact = function(self, collisions)
--For each collision...
for _, c in pairs(collisions) do
--For each flammable node it hit...
if c.type == "node" and minetest.get_item_group(minetest.get_node(c.node_pos).name, "flammable") > 0 then
--Replace that node with fire.
minetest.set_node(c.node_pos, {name = "fire:basic_flame"})
end
end
end
})
end
--Arrows made from gold, which have super high velocity and good damage.
projectile.register_projectile("projectile:arrow_high_velocity", "bow", "projectile:arrow_high_velocity", {
damage = 15,
speed = 70,
initial_properties = {
visual = "mesh",
mesh = "projectile_arrow.obj",
textures = {"projectile_arrow_high_velocity_texture.png"}
},
_on_step = projectile.autorotate_arrow
})
--If the tnt mod is present...
if tnt then
--Register arrows that explode upon contact with anything.
projectile.register_projectile("projectile:arrow_bomb", "bow", "projectile:arrow_bomb", {
--Instead of dealing direct damage, bomb arrows rely on the explosion to deal damage.
damage = 0,
--Also, it's not like fat sticks of tnt are particularly aerodynamic.
speed = 25,
initial_properties = {
visual = "mesh",
mesh = "projectile_arrow_bomb.obj",
textures = {"projectile_arrow_bomb_texture.png"}
},
_on_step = projectile.autorotate_arrow,
--Upon impact, create a small explosion.
on_impact = function(self, collisions)
local pos = self.object:get_pos()
tnt.boom(pos, {radius = 2, damage_radius = 2})
if node_damage then
local r = 4
for a = -r, r do
for b = -r, r do
for c = -r, r do
local npos = vector.add(pos, {x=a, y=b, z=c})
local dist = vector.distance(pos, npos)
local num = math.random(0, math.ceil(r / math.max(1, dist)))
if dist <= r and num > 0 then
node_damage.damage(npos, nil, nil, num)
end
end
end
end
end
end
})
end
--Basic flintlock ammunition with decent damage, and extremely high speed, and slight spread.
projectile.register_projectile("projectile:musket_ball", "flintlock", "projectile:musket_ball", {
damage = 15,
speed = 250,
spread = 2.5,
image = "projectile_dot.png^[multiply:#AEAEAE"
})
--A weaker flintlock ammunition that bursts on impact, striking all enemies in a small radius.
--Note that damage of the initial hit is set to 0 so that the radius damage doesn't cause this bullet to hit the same enemy twice.
projectile.register_projectile("projectile:musket_ball_diamond", "flintlock", "projectile:musket_ball_diamond", {
damage = 0,
speed = 275,
spread = 5,
image = "projectile_dot.png^[multiply:#5AAFE7",
on_impact = function(self, collisions)
--Center the blast on the first node or object that the projectile hit.
local pos = collisions[1].node_pos or collisions[1].object:get_pos()
--For each object in a radius of 2.5 meters/nodes...
for _, target in pairs(minetest.get_objects_inside_radius(pos, 2.5)) do
--As long as this wouldn't be self-damage or friendly fire...
if not target:is_player() or (self.owner ~= target:get_player_name() and projectile.in_same_party(self, target)) then
--Punch that target for 9 damage.
--The direction is just 0, so affected targets won't be knocked anywhere.
--Instead, they'll just freeze in place for a moment.
target:punch(self.object, 1, {full_punch_interval = 1, damage_groups = {fleshy = 9}}, {x=0, y=0, z=0})
end
end
--Then, create 16 little particles.
for i = 1, 16 do
--ps = particle speed.
local ps = 25
--Make a completely random velocity for each particle.
local vel = {x = math.random(-ps, ps), y = math.random(-ps, ps), z = math.random(-ps, ps)}
--Multiply and normalize are used so that every particle has exactly 25 velocity
vel = vector.multiply(vector.normalize(vel), ps)
--Add the particle so that it lasts only long enough to travel 2.5 meters. Have it look like a smaller version of the bullet fired.
minetest.add_particle(pos, vel, {x=0, y=0, z=0}, 2.5 / ps, 1, false, "projectile_dot.png^[multiply:#5AAFE7")
end
end
})
--An upgrade to the musket ball that deal a lot more damage and is slightly faster and more accurate.
projectile.register_projectile("projectile:musket_ball_mithril", "flintlock", "projectile:musket_ball_mithril", {
damage = 24,
speed = 300,
spread = 2,
image = "projectile_dot.png^[multiply:#8282D5"
})
--A standard shotgun blast, with smaller than average pellets that basically tickle enemies if only one or two land.
projectile.register_projectile("projectile:shot_pile", "flintlock_shot", "projectile:shot_pile", {
damage = 4,
speed = 250,
count = 9,
spread = 22.5,
collide_self = false,
initial_properties = {
collisionbox = {-.125, 0, -.125, .125, .25, .125},
visual_size = {x = 0.25, y = 0.25, z = 0.25}
},
image = "projectile_dot.png^[multiply:#AEAEAE"
})
--A massive burst of shot that deals less damage at range, but much more damage up close. Has a lot wider spread as well.
projectile.register_projectile("projectile:shot_pile_diamond", "flintlock_shot", "projectile:shot_pile_diamond", {
damage = 3,
speed = 275,
count = 18,
spread = 30,
collide_self = false,
initial_properties = {
collisionbox = {-.125, 0, -.125, .125, .25, .125},
visual_size = {x = 0.25, y = 0.25, z = 0.25}
},
image = "projectile_dot.png^[multiply:#5AAFE7"
})
--Stronger shot than steel with nearly double the damage and slightly better speed as well.
projectile.register_projectile("projectile:shot_pile_mithril", "flintlock_shot", "projectile:shot_pile_mithril", {
damage = 6,
speed = 300,
count = 9,
spread = 22.5,
collide_self = false,
initial_properties = {
collisionbox = {-.125, 0, -.125, .125, .25, .125},
visual_size = {x = 0.25, y = 0.25, z = 0.25}
},
image = "projectile_dot.png^[multiply:#8282D5"
})