projectile/init.lua

536 lines
20 KiB
Lua
Raw Normal View History

2020-07-13 00:43:15 +02:00
--[[
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 the registration of ammo items that this mod provides, as well as crafting recipes for weapons and ammo.
dofile(mp.."crafts.lua")
--A function that creates and launches a function out of a player's side when they use a projectile weapon.
function projectile.shoot(wep, user, level)
--Some useful shorthands
local pname = user:get_player_name()
local inv = user:get_inventory()
local def = wep:get_definition()
--A projectile isn't spawned directly inside a player, and it doesn't come from the center of the screen.
--It does start directly in front of the player...
local pos = user:get_look_dir()
--But then it's shifted to the right of the player, where it looks like the weapon is held.
pos = vector.rotate(pos, {x=0 , y = -math.pi / 4, z=0})
--Then it's shifted up by the player's face.
pos.y = 1
--The user's actual position is added last, to make rotating easier.
pos = vector.add(pos, user:get_pos())
--Charge level depends on how long the player waited before firing. 1 = 100% charge.
level = math.min(level / def.charge_time, 1)
--Look through each inventory slot...
for i = 1, inv:get_size("main") do
--get the stack itself
local ammo = inv:get_stack("main", i)
--If there is an item stack, and it's registered as an ammo type that this weapon can use...
if not ammo:is_empty() and projectile.registered_projectiles[def.rw_category][ammo:get_name()] then
--Create the projectile entity at the determined position
local projectile = minetest.add_entity(pos, projectile.registered_projectiles[def.rw_category][ammo:get_name()])
--A shorthand of the luaentity version of the projectile, where data can easily be stored
local luapro = projectile:get_luaentity()
--Set velocity according to the direction it was fired. Speed is determined by the weapon, ammo, and how long the weapon was charged.
projectile:set_velocity(vector.multiply(user:get_look_dir(), luapro.speed * level * def.speed))
--An acceleration of -9.81y is how gravity is applied.
projectile:set_acceleration({x=0, y=-9.81, z=0})
--Store level for later, to determine impact damage
luapro.level = level
--Also store the projectile's damage itself.
luapro.damage = def.damage
--The player's name is stored to prevent hitting yourself
--And by "hitting yourself" I mean accidentally being hit by the arrow just by firing it at a somewhat low angle, the moment it spawns.
luapro.owner = pname
--If the player isn't in creative mode, some weapon durability and ammo is consumed.
if not minetest.is_creative_enabled(pname) then
ammo:take_item(1)
inv:set_stack("main", i, ammo)
wep:add_wear(65536 / (def.durability or 100))
end
--Once the ammo is found, the search is stopped.
break
end
end
return wep
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...
if player:get_wield_index() ~= projectile.charge_levels[pname].slot then
--Get the projectile weapon. get_wielded_item() can't be used, since the weapon is no longer held.
local wep = player:get_inventory():get_stack("main", projectile.charge_levels[pname].slot)
--Replace the weapon with the uncharged version
wep:set_name(wep:get_definition().no_charge_name)
player:get_inventory():set_stack("main", projectile.charge_levels[pname].slot, wep)
--Delete the stored charge data for this player
projectile.charge_levels[pname] = nil
--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)
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)
--This function registers a weapon able to shoot projectiles
function projectile.register_weapon(name, def)
--either create a groups table for the definition, or use the provided one
def.groups = def.groups or {}
--Every projectile weapon belongs to the projectile_weapon group.
def.groups.projectile_weapon = 1
--Charge time defaults to 1 second
def.charge_time = def.charge_time or 1
--The weapon's damage multiplier defaults to 1.
def.damage = def.damage or 1
--The weapon's speed multiplier defaults to 1.
def.speed = def.speed or 1
--If this weapons has to be charged...
if def.charge then
--Define a function to reset the weapon's sprite and delete the player's charge data.
local uncharge = function(wep, user)
projectile.charge_levels[user:get_player_name()] = nil
wep:set_name(name)
return wep
end
--A function that begins a new charge, or fires a shot if the player is charging.
local charge = function(wep, user)
local pname = user:get_player_name()
--If there is no charge data yet...
if not projectile.charge_levels[pname] then
local inv = user:get_inventory()
--Look for ammo in the player's inventory, starting from the first slot.
for i = 1, inv:get_size("main") do
--Get the itemstack of the current slot.
local ammo = inv:get_stack("main", i)
--If the stack is there, and it's registered as ammo that this weapon can use...
if not ammo:is_empty() and projectile.registered_projectiles[def.rw_category][ammo:get_name()] then
--Create new charge data. Store the inventory slot of the weapon, and start the charge at 0
projectile.charge_levels[pname] = {slot = user:get_wield_index(), charge = 0}
--As feedback for the charge beginning, change the weapon's sprite to show it loaded.
--I originally wanted the item to be shown loaded with specific ammo, but it doesn't seem to be possible.
wep = ItemStack({name = name.."_2", wear = wep:get_wear()})
--Once ammo is found, the search can be stopped.
break
end
end
--If no ammo was found, a charge won't start at all. No dry-firing allowed.
--Otherwise, if there is charge data...
else
--Shoot out the projectile
projectile.shoot(wep, user, projectile.charge_levels[pname].charge)
--Then, end the charge
wep = uncharge(wep, user)
end
return wep
end
--Right-click to start a charge. Right-click again to fire.
def.on_place = charge
def.on_secondary_use = charge
--Left-click to cancel a charge without firing.
def.on_use = uncharge
--Start the creating of the partially and fully charged versions of this item, first by copying the definition.
local def2 = table.copy(def)
local def3 = table.copy(def)
--The partially and fully-charged versions have specific inventory images
def2.inventory_image = def.inventory_image_2
def3.inventory_image = def.inventory_image_3
--The projectile_weapon group rating increases with charge level
def2.groups.projectile_weapon = 2
def3.groups.projectile_weapon = 3
--Partially charged weapons cannot be grabbed from the creative inventory.
def2.groups.not_in_creative_inventory = 1
def3.groups.not_in_creative_inventory = 1
--Some versions store the names of different versions, for convenience.
--The partially-charged version stores the name of the fully charged version, to be used when transitioning to the fully charged version.
def2.full_charge_name = name.."_3"
--Full and partial charge versions can both be cancelled, so they remember the name of the uncharged version
def2.no_charge_name = name
def3.no_charge_name = name
--Finally, register the partially and fully charged projectile weapons.
minetest.register_tool(name.."_2", def2)
minetest.register_tool(name.."_3", def3)
--Otherwise, right-click simply shoots the projectile.
else
def.on_place = projectile.shoot
def.on_secondary_use = projectile.shoot
end
--Finally, register the projectile weapon here.
--This is the only thing that happens regardless of if the weapon has to charge or not.
minetest.register_tool(name, def)
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
})
--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,
charge_time = 1.1,
damage = 1.25,
speed = 1.75
})
--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,
charge_time = 2
})
--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,
charge_time = 2.1,
damage = 1.5,
speed = 1.9
})
--Register a projectile that can be fired by a weapon.
--Note that it also has to define what kind of weapon can fire it, and the item version of itself.
function projectile.register_projectile(name, usable_by, ammo, def)
--First, check that a table exists for that particular weapon category. If not, make it.
projectile.registered_projectiles[usable_by] = projectile.registered_projectiles[usable_by] or {}
--Then, add this projectile to said table.
projectile.registered_projectiles[usable_by][ammo] = name
--Default initial properties for the projectile
--Including the table itself, if it wasn't already created.
def.initial_properties = def.initial_properties or {}
--The projectile is always physical. It has to hit stuff, after all.
def.initial_properties.physical = true
--The projectile also definitely has to be able to hit other entities.
def.initial_properties.collide_with_objects = true
--By default, the projectile's hitbox is half a block in size.
def.initial_properties.collisionbox = def.initial_properties.collisionbox or {-.25, 0, -.25, .25, .5, .25}
--The projectile can't be hit by players.
def.initial_properties.pointable = false
--By default, the projectile is a flat image, provided by the "image" field.
def.initial_properties.visual = def.initial_properties.visual or "sprite"
def.initial_properties.textures = def.initial_properties.textures or {def.image}
--By default, the projectile's visual size is also half size.
def.initial_properties.visual_size = def.initial_properties.visual_size or {x = 0.5, y = 0.5, z = 0.5}
--The projectile should always have some kind of visual.
def.initial_properties.is_visible = true
--The projectile won't be saved if it becomes unloaded.
def.initial_properties.static_save = false
--During each of this entity's steps...
def.on_step = function(self, dtime, info)
--Let projectiles define their own on_step if they need to
if self._on_step then
self._on_step(self, dtime, info)
end
--A little shorthand
local selfo = self.object
--By default, assume nothing was hit this step.
local hit = false
--For each collision that was found...
for k, c in pairs(info.collisions) do
--If it's a node, don't do anything more than acknowledging that something was hit.
if c.type == "node" and minetest.get_node(c.node_pos).name ~= "default:glass" then
hit = true
--If it's an object...
else
--As long as that object isn't the player who fired this projectile...
if not (c.object:is_player() and self.owner == c.object:get_player_name()) then
hit = true
c.object:punch(selfo, 1, {full_punch_interval = 1, damage_groups = {fleshy = def.damage * self.level * self.damage}}, vector.normalize(selfo:get_velocity()))
end
end
end
--If this projectile hit something...
if hit then
--Grant the entity an on_impact function that it can define
if self.on_impact then
self:on_impact(info.collisions)
end
--Make the projectile destroy itself.
selfo:remove()
end
end
--Finally, register the entity.
minetest.register_entity(name, def)
end
--The basic slingshot projectile: rocks from hardtrees
projectile.register_projectile("projectile:rock", "slingshot", "hardtrees:rock", {
image = "rock_lump.png",
damage = 5,
speed = 15
})
--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
})
--A helper functions for arrows in general, as they rotate themselves according to how they move.
local function arrow_on_step(self)
--Shorthand for velocity
local v = self.object:get_velocity()
--Set calculate rotation according to velocity
local rot = vector.dir_to_rotation(v)
--Define a timer for itself. Based on how fast its currently moving, and how far the timer has progressed,
--this makes it seem to spin through the air, with the tip still always pointing forward.
self.timer = (self.timer or 0) + (v.x + v.y + v.z) / 30
rot.z = rot.z + self.timer
--Apply the calculated rotation.
self.object:set_rotation(rot)
end
--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 = arrow_on_step
})
--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 = arrow_on_step,
--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 = arrow_on_step
})
--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 = arrow_on_step,
--Upon impact, create a small explosion.
on_impact = function(self, collisions)
tnt.boom(self.object:get_pos(), {radius = 2, damage_radius = 2})
end
})
end