Update 1.1.0
408
api.lua
Normal file
@ -0,0 +1,408 @@
|
||||
--[[
|
||||
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
|
||||
--]]
|
||||
|
||||
--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
|
||||
|
||||
--A function that determines when a weapon can fire. If none is provided in a definition, a weapon can always fire.
|
||||
--Note that it does not prevent the need for ammunition.
|
||||
def.can_fire = def.can_fire or function() return true end
|
||||
|
||||
--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, cancelled)
|
||||
--If nothing was fired and the weapon defines an on_cancel function, call it.
|
||||
if cancelled and def.on_cancel then
|
||||
def.on_cancel(wep, user)
|
||||
end
|
||||
|
||||
--Delete charge data.
|
||||
projectile.charge_levels[user:get_player_name()] = nil
|
||||
|
||||
--Change the name of the stack to transform it back to its uncharged form.
|
||||
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)
|
||||
--If it is allowed to fire...
|
||||
if def.can_fire(wep, user) then
|
||||
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
|
||||
if def.on_fire then
|
||||
def.on_fire(wep, user)
|
||||
end
|
||||
|
||||
--Shoot out the projectile
|
||||
projectile.shoot(wep, user, projectile.charge_levels[pname].charge)
|
||||
|
||||
if def.after_fire then
|
||||
def.after_fire(wep, user)
|
||||
end
|
||||
|
||||
--Then, end the charge
|
||||
uncharge(wep, user, false)
|
||||
end
|
||||
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
|
||||
|
||||
--Weapons that need charging can only be fired before fully charging if def.fire_while_charging is set to true.
|
||||
if def.charge and not def.fire_while_charging then
|
||||
def2.on_place = nil
|
||||
def2.on_secondary_use = nil
|
||||
end
|
||||
|
||||
--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)
|
||||
else
|
||||
--Otherwise, right-click simply shoots the projectile.
|
||||
def.on_place = function(wep, user)
|
||||
--If it is allowed to fire...
|
||||
if def.can_fire(wep, user) then
|
||||
--Shoot every time on_place is called.
|
||||
projectile.shoot(wep, user)
|
||||
end
|
||||
end
|
||||
|
||||
def.on_secondary_use = def.on_place
|
||||
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
|
||||
|
||||
--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 won't be saved if it becomes unloaded.
|
||||
def.initial_properties.static_save = false
|
||||
|
||||
--collide_self allows projectiles fired by the same person to strike each other. This is true by default.
|
||||
if def.collide_self == nil then
|
||||
def.collide_self = true
|
||||
end
|
||||
|
||||
--"visible" can be used as a shorthand to set itself in initial_properties.
|
||||
if def.visible == false then
|
||||
def.initial_properties.is_visible = false
|
||||
end
|
||||
|
||||
--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
|
||||
|
||||
--selfo:get_pos() is used to know if remove() was called in the _on_step function.
|
||||
if not selfo:get_pos() then
|
||||
return
|
||||
end
|
||||
|
||||
--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" then
|
||||
hit = true
|
||||
|
||||
--If it's an object...
|
||||
else
|
||||
--As long as that object isn't the player who fired this projectile and the target isn't already dead...
|
||||
--And the target isn't a projectile owned by the same player when collide_self is disabled...
|
||||
--And the target isn't in the same party as this projectile's owner...
|
||||
if not (c.object:is_player() and self.owner == c.object:get_player_name()) and c.object:get_hp() > 0
|
||||
and not (self.collide_self == false and not c.object:is_player() and self.owner == c.object:get_luaentity().owner)
|
||||
and not projectile.in_same_party(self, c.object) then
|
||||
--Acknowledge the hit
|
||||
hit = true
|
||||
--Deal damage to the target.
|
||||
c.object:punch(selfo, 1, {full_punch_interval = 1, damage_groups = {fleshy = def.damage * self.level * self.damage}}, vector.normalize(selfo:get_velocity()))
|
||||
else
|
||||
--Otherwise, pass by the object as best as possible.
|
||||
selfo:set_velocity(self.oldvel)
|
||||
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
|
||||
|
||||
--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
|
||||
local adef = minetest.registered_entities[ammo:get_name()]
|
||||
|
||||
--Fire an amount of projectiles at once according to the ammo's defined "count".
|
||||
for n = 1, (adef.count or 1) do
|
||||
--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})
|
||||
|
||||
--If the ammo defines a spread, randomly rotate the direction of velocity by that given radius.
|
||||
if adef.spread then
|
||||
local rx = (math.random() * adef.spread * 2 - adef.spread) * math.pi / 180
|
||||
local ry = (math.random() * adef.spread * 2 - adef.spread) * math.pi / 180
|
||||
|
||||
projectile:set_velocity(vector.rotate(projectile:get_velocity(), {x = rx, y = ry, z = 0}))
|
||||
end
|
||||
|
||||
--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
|
||||
--Store the initial velocity for passing by objects when needed.
|
||||
luapro.oldvel = projectile:get_velocity()
|
||||
end
|
||||
|
||||
--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
|
||||
|
||||
--A helper function to know when the party of a projectile's owner and target is the same.
|
||||
function projectile.in_same_party(projectile, target)
|
||||
--Automatically return false if:
|
||||
--The parties mod isn't included.
|
||||
--The target isn't a player.
|
||||
--The projectile's target and/or owner isn't in a party/
|
||||
if not parties or not target:is_player() or not parties.is_player_in_party(projectile.owner) or not parties.is_player_in_party(target:get_player_name()) then
|
||||
return false
|
||||
end
|
||||
|
||||
--Return true only if the projectile's owner and target and under the same party leadership.
|
||||
return parties.get_party_leader(projectile.owner) == parties.get_party_leader(target:get_player_name())
|
||||
end
|
||||
|
||||
--A helper functions for arrows in general, as they rotate themselves according to how they move.
|
||||
function projectile.autorotate_arrow(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
|
||||
|
||||
--A can_fire function for flintlock weapons that enforces a gunpowder requirement, in addition to the usual ammo needs
|
||||
function projectile.needs_gunpowder(wep, user)
|
||||
--Automatically return true if creative mode is enabled or the weapon is already firing.
|
||||
if minetest.is_creative_enabled(user:get_player_name()) or projectile.charge_levels[user:get_player_name()] then
|
||||
return true
|
||||
end
|
||||
|
||||
--Shorthand to get the user's inventory
|
||||
local inv = user:get_inventory()
|
||||
|
||||
--For each slot in the user's inventory...
|
||||
for i = 1, inv:get_size("main") do
|
||||
--Get the current stack at this index.
|
||||
local stack = inv:get_stack("main", i)
|
||||
|
||||
--If this stack contains gunpowder...
|
||||
if stack:get_name() == "tnt:gunpowder" then
|
||||
--Consume some.
|
||||
stack:take_item()
|
||||
--Update the inventory.
|
||||
inv:set_stack("main", i, stack)
|
||||
|
||||
--Allow firing.
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
--If no gunpowder could be consumed, disallow firing.
|
||||
return false
|
||||
end
|
||||
|
||||
--A flintlock's can_fire function consumes gunpowder before letting the weapon charge.
|
||||
--So, if that charge was cancelled, gunpowder should be returned.
|
||||
function projectile.return_gunpowder(wep, user)
|
||||
--In creative mode, nothing gets taken, so nothing needs to be returned.
|
||||
if not minetest.is_creative_enabled(user:get_player_name()) then
|
||||
--Try adding the gunpowder back into the inventory. Store the leftover stack.
|
||||
local leftover = user:get_inventory():add_item("main", ItemStack({name = "tnt:gunpowder"}))
|
||||
|
||||
--If the gunpowder couldn't be added due to the inventory being full...
|
||||
if not leftover:is_empty() then
|
||||
--Drop it on the ground instead.
|
||||
minetest.add_item(user:get_pos(), leftover)
|
||||
end
|
||||
end
|
||||
end
|
162
api.txt
Normal file
@ -0,0 +1,162 @@
|
||||
Tables
|
||||
------
|
||||
|
||||
projectile.registered_projectiles
|
||||
A per-category list of lists, the categories being types of ranged weapons like "bow", "slingshot", etc.
|
||||
Within each category, key-value pairs are listed to link together the entity and item forms of ammo
|
||||
The item's name is the key, the entity's name is the value.
|
||||
|
||||
projectile.charge_levels
|
||||
A per-player list of lists that keeps track of data regarding ranged weapons that are currently being used.
|
||||
Whenever a weapon charge-up is cancelled, the charge_levels entry for that player will be cleared.
|
||||
When a weapon is being charged up, a table for that weapon's user is created containing the following:
|
||||
{
|
||||
slot = <index of wielded ranged weapon>,
|
||||
charge = <number>
|
||||
}
|
||||
|
||||
|
||||
|
||||
Regristration Functions
|
||||
-----------------------
|
||||
|
||||
projectile.register_weapon(name, definition)
|
||||
--Register a new ranged weapon
|
||||
--Inherits from minetest.register_tool
|
||||
|
||||
description = "Description",
|
||||
|
||||
inventory_image = "image.png",
|
||||
inventory_image_2 = "image_charged.png",
|
||||
inventory_image_3 = "image_charged_full.png",
|
||||
--When creating a weapon that can be charged, you should provide different sprites when charging the weapon
|
||||
--and when the weapon is fully charged.
|
||||
|
||||
durability = 100,
|
||||
--Defines how many times this weapon can be used before breaking.
|
||||
|
||||
rw_category = "category",
|
||||
--A projectile weapon may only fire ammo types in the same category.
|
||||
--This mod provides "bow", "flintlock", and "slingshot"
|
||||
--You can use a custom category, as long as you also register custom ammo
|
||||
|
||||
charge = false,
|
||||
--If true, right clicking will put the weapon in a charging state.
|
||||
--Once charging, right-click again to fire, or left-click to cancel.
|
||||
--If a weapon doesn't charge, right-clicking will always fire.
|
||||
--Defaults to false
|
||||
|
||||
fire_while_charging = false,
|
||||
--If true, the weapon doesn't need to be fully charged to be fired.
|
||||
--A partially charged shot will still be weaker than a fully charged one, however.
|
||||
--Does nothing if charge is false
|
||||
--Defaults to false
|
||||
|
||||
charge_time = 1,
|
||||
--The amount of time in seconds to finish charging.
|
||||
--Does nothing if charge is false
|
||||
--Defaults to false.
|
||||
|
||||
damage = 1,
|
||||
--A damage multiplier applied to fired projectiles
|
||||
|
||||
speed = 1,
|
||||
--A multiplier to the projectile's initial velocity
|
||||
|
||||
can_fire = function(weapon, user)
|
||||
--Use this to create extra conditions for when a weapon can be fired.
|
||||
--Can't be used to negate the need for ammo.
|
||||
--Defaults to always return true.
|
||||
|
||||
on_cancel = function(weapon, user)
|
||||
--This function is called whenever a charge is cut short, either by left-clicking, switching the selected hotbar index, or leaving the game.
|
||||
--Has no return value.
|
||||
|
||||
on_fire = function(wep, user)
|
||||
--This function is called just before the projectile is created.
|
||||
--Has no return value.
|
||||
|
||||
after_fire = function(weapon, user)
|
||||
--This function is called just after the projectile is created.
|
||||
--Has no return value.
|
||||
|
||||
|
||||
|
||||
projectile.register_projectile(name, usable_by, ammo, definition)
|
||||
--Register a new projectile entity and an associated ammo item.
|
||||
--Note that this function will NOT register a new item for you. Use one of minetest's normal item registration functions instead.
|
||||
--Inherits from minetest.register_entity.
|
||||
--usable_by: This should match the rw_category of the weapon that you want to use this ammo.
|
||||
--ammo: The name of the item that is consumed to create this projectile.
|
||||
|
||||
image = "image.png",
|
||||
--A shortcut for initial_properties.texture. You can ignore this if you define a mesh for the projectile.
|
||||
|
||||
damage = 5,
|
||||
--The base damage that this projectile deals.
|
||||
|
||||
speed = 15,
|
||||
--The base initial velocity of this projectile, in meters/nodes per second.
|
||||
|
||||
count = 1,
|
||||
--The amount of projectiles that is created per shot.
|
||||
--Meant for shotgun-like effects.
|
||||
--defaults to 1
|
||||
|
||||
spread = 0,
|
||||
--The radius, in degrees, that projectiles can spread away from the player's look direction.
|
||||
--Defaults to 0
|
||||
|
||||
collide_self = true,
|
||||
--As long as its true, a player can shoot their own projectiles.
|
||||
--If false, two projectiles owned by the same player will phase through each other.
|
||||
--Defaults to true.
|
||||
|
||||
_on_step = function(self, dtime, moveresult)
|
||||
--Use this function to give your projectile an on_step callback.
|
||||
--If you try to use the regular on_step, it will be overwritten.
|
||||
|
||||
on_impact = function(self, collisions)
|
||||
--This function is called when a node or object is struck.
|
||||
--Has no return value.
|
||||
--collisions is a table taken from on_step's moveresult, which contains the following:
|
||||
{
|
||||
type = string, -- "node" or "object",
|
||||
axis = string, -- "x", "y" or "z"
|
||||
node_pos = vector, -- if type is "node"
|
||||
object = ObjectRef, -- if type is "object"
|
||||
old_velocity = vector,
|
||||
new_velocity = vector,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Misc Functions
|
||||
--------------
|
||||
|
||||
projectile.shoot(wep, user, level)
|
||||
--Shoot out a projectile at the user's position.
|
||||
--Afterwards, deplete the ammo used and add wear to the weapon used.
|
||||
--level: The charge level at the time of firing.
|
||||
|
||||
function projectile.in_same_party(projectile, target)
|
||||
--If the parties mod is enabled, this checks if the projectile's owner is in the same party as the target.
|
||||
--Always returns false if the parties mod is not present.
|
||||
|
||||
projectile.autorotate_arrow(self)
|
||||
--Meant to be given to an arrow projectile's _on_step method, when that arrow uses a mesh.
|
||||
--This causes the arrow to spin as it travels.
|
||||
--Spin speed is dependent on velocity.
|
||||
|
||||
projectile.needs_gunpowder(wep, user)
|
||||
--Meant to be given to a flintlock's can_fire function.
|
||||
--Searches the user's main inventory for tnt:gunpowder.
|
||||
--If none is found, return false to prevent firing.
|
||||
--If some is found, consume it and return true.
|
||||
--If the player is already charging, skip this check and automatically return true. No need to take two gunpowder.
|
||||
|
||||
projectile.return_gunpowder(wep, user)
|
||||
--Meant to be used with a flintlock's on_cancel function.
|
||||
--projectile.needs_gunpowder takes gunpowder right before a charge is started.
|
||||
--This means that gunpowder needs to be added back to the inventory if the charge is cancelled.
|
||||
--If no space is left in the inventory for the gunpowder, drop it on the ground instead.
|
1
bugs.txt
Normal file
@ -0,0 +1 @@
|
||||
projectile isn't unloaded when player leaves
|
22
changelog.txt
Normal file
@ -0,0 +1,22 @@
|
||||
v1.1.0:
|
||||
Added flintlock weapons:
|
||||
+Flintlock pistols, muskets, and blunderbusses
|
||||
+Optional dependencies on mesecons_walllever, moreores, and pipeworks for crafting recipes.
|
||||
Required musket balls as ammo, crafted from steel, diamond, or mithril.
|
||||
Blunderbusses require shot piles, crafted from three musket balls.
|
||||
Each requires gunpowder in order to fire, in addition to ammunition.
|
||||
They have a loading period similar to bows and slingshots, but cannot be fired until fully charged.
|
||||
|
||||
Parties:
|
||||
+Optional dependency for the new parties mod by Zughy
|
||||
+As long as two players are in the same party, their projectiles cannot hurt each other.
|
||||
|
||||
Misc:
|
||||
+Added an api.txt to describe functions that other mods can use to create their own projectile weapons.
|
||||
+Normal arrows now light on fire if they pass through fire or lava.
|
||||
+Fire arrows now extinguish if they pass through water.
|
||||
|
||||
Fixes:
|
||||
-Issues with weapons thinking they're still charged if the player leaves the game, even though they shouldn't be.
|
||||
-Possible crash if a projectile ever called self.object:remove() in its _on_step function.
|
||||
-Dead players will no longer block projectiles.
|
159
crafts.lua
@ -24,12 +24,10 @@ minetest.register_craftitem("projectile:arrow", {
|
||||
})
|
||||
|
||||
--An arrow that burns flammable nodes that it touches
|
||||
if fire then
|
||||
minetest.register_craftitem("projectile:arrow_fire", {
|
||||
description = "Fire Arrow",
|
||||
inventory_image = "projectile_arrow_fire.png",
|
||||
})
|
||||
end
|
||||
minetest.register_craftitem("projectile:arrow_fire", {
|
||||
description = "Fire Arrow",
|
||||
inventory_image = "projectile_arrow_fire.png",
|
||||
})
|
||||
|
||||
--An arrow with exceptionally high velocity
|
||||
minetest.register_craftitem("projectile:arrow_high_velocity", {
|
||||
@ -38,12 +36,46 @@ minetest.register_craftitem("projectile:arrow_high_velocity", {
|
||||
})
|
||||
|
||||
--An arrow that explodes on contact, rather than dealing direct damage.
|
||||
if tnt then
|
||||
minetest.register_craftitem("projectile:arrow_bomb", {
|
||||
description = "Bomb Arrow",
|
||||
inventory_image = "projectile_arrow_bomb.png",
|
||||
})
|
||||
end
|
||||
minetest.register_craftitem("projectile:arrow_bomb", {
|
||||
description = "Bomb Arrow",
|
||||
inventory_image = "projectile_arrow_bomb.png",
|
||||
})
|
||||
|
||||
--Basic flintlock ammo, small steel balls
|
||||
minetest.register_craftitem("projectile:musket_ball", {
|
||||
description = "Musket Ball",
|
||||
inventory_image = "projectile_musket_ball.png",
|
||||
})
|
||||
|
||||
--Easily shattered ammo with less direct damage than a musket ball, but much greater crowd control abilities.
|
||||
minetest.register_craftitem("projectile:musket_ball_diamond", {
|
||||
description = "Diamond Musket Ball",
|
||||
inventory_image = "projectile_musket_ball_diamond.png",
|
||||
})
|
||||
|
||||
--An upgrade to regular musket balls
|
||||
minetest.register_craftitem("projectile:musket_ball_mithril", {
|
||||
description = "Mithril Musket Ball",
|
||||
inventory_image = "projectile_musket_ball_mithril.png",
|
||||
})
|
||||
|
||||
--Standard shotgun ammo, a pile of 9 tiny balls
|
||||
minetest.register_craftitem("projectile:shot_pile", {
|
||||
description = "Shot Pile",
|
||||
inventory_image = "projectile_shot_pile.png",
|
||||
})
|
||||
|
||||
--Easily shattered ammo with less direct damage than shot, but each ball splits into 2 in the initial blast, causing a total of 18 pellets to be spewed.
|
||||
minetest.register_craftitem("projectile:shot_pile_diamond", {
|
||||
description = "Diamond Shot Pile",
|
||||
inventory_image = "projectile_shot_pile_diamond.png",
|
||||
})
|
||||
|
||||
--An upgrade to regular shot.
|
||||
minetest.register_craftitem("projectile:shot_pile_mithril", {
|
||||
description = "Mithril Shot Pile",
|
||||
inventory_image = "projectile_shot_pile_mithril.png",
|
||||
})
|
||||
|
||||
|
||||
|
||||
@ -69,9 +101,9 @@ minetest.register_craft({
|
||||
minetest.register_craft({
|
||||
output = "projectile:slingshot",
|
||||
recipe = {
|
||||
{"", "default:stick", "farming:string"},
|
||||
{"", "default:stick", "default:stick"},
|
||||
{"default:stick", "", ""}
|
||||
{"", "group:stick", "farming:string"},
|
||||
{"", "group:stick", "group:stick"},
|
||||
{"group:stick", "", ""}
|
||||
}
|
||||
})
|
||||
|
||||
@ -90,8 +122,8 @@ minetest.register_craft({
|
||||
minetest.register_craft({
|
||||
output = "projectile:bow",
|
||||
recipe = {
|
||||
{"default:stick", "default:stick", "farming:string"},
|
||||
{"default:stick", "farming:string", ""},
|
||||
{"group:stick", "group:stick", "farming:string"},
|
||||
{"group:stick", "farming:string", ""},
|
||||
{"farming:string", "", ""}
|
||||
}
|
||||
})
|
||||
@ -107,6 +139,38 @@ minetest.register_craft({
|
||||
}
|
||||
})
|
||||
|
||||
--Flintlocks are made from metal and steel, with a pipe segment for the barrel and a lever for the trigger.
|
||||
minetest.register_craft({
|
||||
output = "projectile:flintlock_pistol",
|
||||
recipe = {
|
||||
{"pipeworks:pipe_1_empty", ""},
|
||||
{"mesecons_walllever:wall_lever_off", "default:steel_ingot"},
|
||||
{"group:stick", ""}
|
||||
}
|
||||
})
|
||||
|
||||
--Muskets add an extra barrel because they're long.
|
||||
minetest.register_craft({
|
||||
output = "projectile:musket",
|
||||
recipe = {
|
||||
{"pipeworks:pipe_1_empty", "", ""},
|
||||
{"", "pipeworks:pipe_1_empty", "default:steel_ingot"},
|
||||
{"", "mesecons_walllever:wall_lever_off", "group:stick"}
|
||||
}
|
||||
})
|
||||
|
||||
--Blunderbusses are thucker, so a second steel ingot is used in place of a pipe segment.
|
||||
minetest.register_craft({
|
||||
output = "projectile:blunderbuss",
|
||||
recipe = {
|
||||
{"default:steel_ingot", ""},
|
||||
{"mesecons_walllever:wall_lever_off", "default:steel_ingot"},
|
||||
{"group:stick", ""}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
--Regular arrows are made from flint, a stick, and a feather.
|
||||
--The feather can be provided by multiple mob mods.
|
||||
--Arrows are also materials in the stronger ammo options for bows.
|
||||
@ -114,7 +178,7 @@ minetest.register_craft({
|
||||
output = "projectile:arrow",
|
||||
recipe = {
|
||||
{"default:flint", "", ""},
|
||||
{"", "default:stick", ""},
|
||||
{"", "group:stick", ""},
|
||||
{"", "", "mobs:chicken_feather"}
|
||||
}
|
||||
})
|
||||
@ -122,7 +186,7 @@ minetest.register_craft({
|
||||
output = "projectile:arrow",
|
||||
recipe = {
|
||||
{"default:flint", "", ""},
|
||||
{"", "default:stick", ""},
|
||||
{"", "group:stick", ""},
|
||||
{"", "", "animalmaterials:feather"}
|
||||
}
|
||||
})
|
||||
@ -130,19 +194,17 @@ minetest.register_craft({
|
||||
output = "projectile:arrow",
|
||||
recipe = {
|
||||
{"default:flint", "", ""},
|
||||
{"", "default:stick", ""},
|
||||
{"", "group:stick", ""},
|
||||
{"", "", "creatures:feather"}
|
||||
}
|
||||
})
|
||||
|
||||
--Combining an arrow with a torch lights it on fire.
|
||||
if fire then
|
||||
minetest.register_craft({
|
||||
type = "shapeless",
|
||||
output = "projectile:arrow_fire",
|
||||
recipe = {"projectile:arrow", "default:torch"}
|
||||
})
|
||||
end
|
||||
minetest.register_craft({
|
||||
type = "shapeless",
|
||||
output = "projectile:arrow_fire",
|
||||
recipe = {"projectile:arrow", "default:torch"}
|
||||
})
|
||||
|
||||
--Gold tools are often fast, so gold arrows focus on being fast.
|
||||
--A gold ingot can turn four arrows gold.
|
||||
@ -158,3 +220,46 @@ minetest.register_craft({
|
||||
output = "projectile:arrow_bomb",
|
||||
recipe = {"projectile:arrow", "tnt:tnt_stick"}
|
||||
})
|
||||
|
||||
--Converting applicable materials to 12 musket balls requires putting 4 of those materials into a ball-esque shape.
|
||||
minetest.register_craft({
|
||||
output = "projectile:musket_ball 12",
|
||||
recipe = {
|
||||
{"", "default:steel_ingot", ""},
|
||||
{"default:steel_ingot", "", "default:steel_ingot"},
|
||||
{"", "default:steel_ingot", ""},
|
||||
}
|
||||
})
|
||||
minetest.register_craft({
|
||||
output = "projectile:musket_ball_diamond 12",
|
||||
recipe = {
|
||||
{"", "default:diamond", ""},
|
||||
{"default:diamond", "", "default:diamond"},
|
||||
{"", "default:diamond", ""},
|
||||
}
|
||||
})
|
||||
minetest.register_craft({
|
||||
output = "projectile:musket_ball_mithril 12",
|
||||
recipe = {
|
||||
{"", "moreores:mithril_ingot", ""},
|
||||
{"moreores:mithril_ingot", "", "moreores:mithril_ingot"},
|
||||
{"", "moreores:mithril_ingot", ""},
|
||||
}
|
||||
})
|
||||
|
||||
--3 of any musket ball can be split into 9 smaller balls, known as a shot pile.
|
||||
minetest.register_craft({
|
||||
type = "shapeless",
|
||||
output = "projectile:shot_pile",
|
||||
recipe = {"projectile:musket_ball", "projectile:musket_ball", "projectile:musket_ball"}
|
||||
})
|
||||
minetest.register_craft({
|
||||
type = "shapeless",
|
||||
output = "projectile:shot_pile_diamond",
|
||||
recipe = {"projectile:musket_ball_diamond", "projectile:musket_ball_diamond", "projectile:musket_ball_diamond"}
|
||||
})
|
||||
minetest.register_craft({
|
||||
type = "shapeless",
|
||||
output = "projectile:shot_pile_mithril",
|
||||
recipe = {"projectile:musket_ball_mithril", "projectile:musket_ball_mithril", "projectile:musket_ball_mithril"}
|
||||
})
|
||||
|
497
init.lua
@ -31,70 +31,32 @@ 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 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 helper function to cancel a player's charge when necessary.
|
||||
local function uncharge_player(player)
|
||||
--Useful shorthand
|
||||
local pname = player:get_player_name()
|
||||
|
||||
--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())
|
||||
--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)
|
||||
|
||||
--Charge level depends on how long the player waited before firing. 1 = 100% charge.
|
||||
level = math.min(level / def.charge_time, 1)
|
||||
--Call the weapon's on_use function, which will cancel it.
|
||||
wep:get_definition().on_use(wep, player, true)
|
||||
|
||||
--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
|
||||
--Update the player's inventory with any modifications.
|
||||
player:get_inventory():set_stack("main", old_slot, wep)
|
||||
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.
|
||||
@ -108,15 +70,8 @@ minetest.register_globalstep(function(dtime)
|
||||
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
|
||||
--Cancel their charge.
|
||||
uncharge_player(player)
|
||||
|
||||
--Otherwise, as long as the player doesn't change weapon...
|
||||
else
|
||||
@ -151,114 +106,19 @@ minetest.register_allow_player_inventory_action(function(player, action, inv, in
|
||||
end
|
||||
end)
|
||||
|
||||
--If a player leaves, cancel their charge.
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
uncharge_player(player)
|
||||
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
|
||||
--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)
|
||||
|
||||
|
||||
--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", {
|
||||
@ -268,7 +128,8 @@ projectile.register_weapon("projectile:slingshot", {
|
||||
inventory_image_3 = "projectile_slingshot_charged_full.png",
|
||||
durability = 75,
|
||||
rw_category = "slingshot",
|
||||
charge = true
|
||||
charge = true,
|
||||
fire_while_charging = true
|
||||
})
|
||||
|
||||
--An upgraded slingshot, which fires faster and harder, but is slightly harder to charge up. Metal wire is stiffer than string, after all.
|
||||
@ -280,6 +141,7 @@ projectile.register_weapon("projectile:steel_slingshot", {
|
||||
durability = 150,
|
||||
rw_category = "slingshot",
|
||||
charge = true,
|
||||
fire_while_charging = true,
|
||||
charge_time = 1.1,
|
||||
damage = 1.25,
|
||||
speed = 1.75
|
||||
@ -294,6 +156,7 @@ projectile.register_weapon("projectile:bow", {
|
||||
durability = 100,
|
||||
rw_category = "bow",
|
||||
charge = true,
|
||||
fire_while_charging = true,
|
||||
charge_time = 2
|
||||
})
|
||||
|
||||
@ -306,85 +169,60 @@ projectile.register_weapon("projectile:steel_bow", {
|
||||
durability = 200,
|
||||
rw_category = "bow",
|
||||
charge = true,
|
||||
fire_while_charging = true,
|
||||
charge_time = 2.1,
|
||||
damage = 1.5,
|
||||
speed = 1.9
|
||||
})
|
||||
|
||||
--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
|
||||
})
|
||||
|
||||
--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
|
||||
})
|
||||
|
||||
--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
|
||||
})
|
||||
|
||||
|
||||
--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", {
|
||||
@ -437,22 +275,6 @@ projectile.register_projectile("projectile:obsidian", "slingshot", "default:obsi
|
||||
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,
|
||||
@ -464,7 +286,31 @@ projectile.register_projectile("projectile:arrow", "bow", "projectile:arrow", {
|
||||
textures = {"projectile_arrow_texture.png"}
|
||||
},
|
||||
|
||||
_on_step = arrow_on_step
|
||||
_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...
|
||||
@ -480,7 +326,29 @@ if fire then
|
||||
textures = {"projectile_arrow_fire_texture.png"}
|
||||
},
|
||||
|
||||
_on_step = arrow_on_step,
|
||||
_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)
|
||||
@ -507,7 +375,7 @@ projectile.register_projectile("projectile:arrow_high_velocity", "bow", "project
|
||||
textures = {"projectile_arrow_high_velocity_texture.png"}
|
||||
},
|
||||
|
||||
_on_step = arrow_on_step
|
||||
_on_step = projectile.autorotate_arrow
|
||||
})
|
||||
|
||||
--If the tnt mod is present...
|
||||
@ -525,7 +393,7 @@ if tnt then
|
||||
textures = {"projectile_arrow_bomb_texture.png"}
|
||||
},
|
||||
|
||||
_on_step = arrow_on_step,
|
||||
_on_step = projectile.autorotate_arrow,
|
||||
|
||||
--Upon impact, create a small explosion.
|
||||
on_impact = function(self, collisions)
|
||||
@ -533,3 +401,108 @@ if tnt then
|
||||
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 = 10,
|
||||
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 6 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 = 6}}, {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 deals nearly double damage and is slightly faster and more accurate.
|
||||
projectile.register_projectile("projectile:musket_ball_mithril", "flintlock", "projectile:musket_ball_mithril", {
|
||||
damage = 18,
|
||||
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 = 3,
|
||||
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 = 2,
|
||||
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 = 5,
|
||||
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"
|
||||
})
|
||||
|
2
mod.conf
@ -1,3 +1,3 @@
|
||||
name = projectile
|
||||
description = Adds a small slingshot that can launch certain small objects as projectiles, such as rocks.
|
||||
optional_depends = animalmaterials, creatures, default, farming, fire, hardtrees, mesecons, mobs, tnt
|
||||
optional_depends = animalmaterials, creatures, default, farming, fire, hardtrees, mesecons_walllever, mesecons, mobs, moreores, parties, pipeworks, tnt
|
||||
|
BIN
textures/projectile_blunderbuss.png
Normal file
After Width: | Height: | Size: 356 B |
BIN
textures/projectile_blunderbuss_charged.png
Normal file
After Width: | Height: | Size: 344 B |
BIN
textures/projectile_bullet.png
Normal file
After Width: | Height: | Size: 305 B |
BIN
textures/projectile_bullet_diamond.png
Normal file
After Width: | Height: | Size: 393 B |
BIN
textures/projectile_bullet_mithril.png
Normal file
After Width: | Height: | Size: 330 B |
BIN
textures/projectile_cartridge_diamond.png
Normal file
After Width: | Height: | Size: 548 B |
BIN
textures/projectile_cartridge_mythril.png
Normal file
After Width: | Height: | Size: 513 B |
BIN
textures/projectile_cartridge_steel.png
Normal file
After Width: | Height: | Size: 511 B |
BIN
textures/projectile_dot.png
Normal file
After Width: | Height: | Size: 126 B |
BIN
textures/projectile_flintlock_pistol.png
Normal file
After Width: | Height: | Size: 326 B |
BIN
textures/projectile_flintlock_pistol_charged.png
Normal file
After Width: | Height: | Size: 324 B |
BIN
textures/projectile_musket.png
Normal file
After Width: | Height: | Size: 317 B |
BIN
textures/projectile_musket_ball.png
Normal file
After Width: | Height: | Size: 483 B |
BIN
textures/projectile_musket_ball_diamond.png
Normal file
After Width: | Height: | Size: 514 B |
BIN
textures/projectile_musket_ball_mithril.png
Normal file
After Width: | Height: | Size: 514 B |
BIN
textures/projectile_musket_charged.png
Normal file
After Width: | Height: | Size: 312 B |
BIN
textures/projectile_shot_pile.png
Normal file
After Width: | Height: | Size: 283 B |
BIN
textures/projectile_shot_pile_diamond.png
Normal file
After Width: | Height: | Size: 542 B |
BIN
textures/projectile_shot_pile_mithril.png
Normal file
After Width: | Height: | Size: 330 B |