throwing/init.lua
upsilon b9c78c92b2
Make the data table different for each arrow
I found myself discovering, while re-reading this code (and judging it not so readable, despite myself being its author), that data table of each arrow was in fact a single reference shared between all entities corresponding to an arrow of the same type.
I'm really sorry about this and the catastrophes it may have caused (e.g., it made it easy to duplicate an item using a drop arrow by throwing a random item, and then throwing the item to duplicate before the first arrow hit the ground: both arrows would then produce the stack that was thrown last). I hope no player discovered this on a server that used this mod.

(Also, I don't really see the point of the make_arrow_def function anymore, so I removed it. I wrote this too long ago to remember why I would write such code.)
2020-02-16 18:20:37 +01:00

364 lines
12 KiB
Lua

throwing = {}
throwing.arrows = {}
throwing.target_object = 1
throwing.target_node = 2
throwing.target_both = 3
throwing.modname = minetest.get_current_modname()
--------- Arrows functions ---------
function throwing.is_arrow(itemstack)
return throwing.arrows[ItemStack(itemstack):get_name()]
end
function throwing.spawn_arrow_entity(pos, arrow, player)
if throwing.is_arrow(arrow) then
return minetest.add_entity(pos, arrow.."_entity")
elseif minetest.registered_items[arrow].throwing_entity then
if type(minetest.registered_items[arrow].throwing_entity) == "string" then
return minetest.add_entity(pos, minetest.registered_items[arrow].throwing_entity)
else -- Type is a function
return minetest.registered_items[arrow].throwing_entity(pos, player)
end
else
obj = minetest.add_entity(pos, "__builtin:item", arrow)
end
end
local function shoot_arrow(itemstack, player, index, throw_itself, new_stack)
local inventory = player:get_inventory()
if not throw_itself then
if index >= player:get_inventory():get_size("main") then
return false
end
index = index + 1
end
local arrow_stack = inventory:get_stack("main", index)
local arrow = arrow_stack:get_name()
local playerpos = player:get_pos()
local pos = {x=playerpos.x,y=playerpos.y+1.5,z=playerpos.z}
local obj = (minetest.registered_items[itemstack:get_name()].spawn_arrow_entity or throwing.spawn_arrow_entity)(pos, arrow, player)
local luaentity = obj:get_luaentity()
-- Set custom data in the entity
luaentity.player = player:get_player_name()
if not luaentity.item then
luaentity.item = arrow
end
luaentity.data = {}
luaentity.timer = 0
if luaentity.on_throw then
if luaentity:on_throw(pos, player, arrow_stack, index, luaentity.data) == false then
obj:remove()
return false
end
end
local dir = player:get_look_dir()
local velocity_factor = tonumber(minetest.settings:get("throwing.velocity_factor")) or 19
local horizontal_acceleration_factor = tonumber(minetest.settings:get("throwing.horizontal_acceleration_factor")) or -3
local vertical_acceleration = tonumber(minetest.settings:get("throwing.vertical_acceleration")) or -10
obj:set_velocity({x=dir.x*velocity_factor, y=dir.y*velocity_factor, z=dir.z*velocity_factor})
obj:set_acceleration({x=dir.x*horizontal_acceleration_factor, y=vertical_acceleration, z=dir.z*horizontal_acceleration_factor})
obj:set_yaw(player:get_look_horizontal()-math.pi/2)
if luaentity.on_throw_sound ~= "" then
minetest.sound_play(luaentity.on_throw_sound or "throwing_sound", {pos=playerpos, gain = 0.5})
end
if not minetest.settings:get_bool("creative_mode") then
if new_stack then
inventory:set_stack("main", index, new_stack)
else
local stack = inventory:get_stack("main", index)
stack:take_item()
inventory:set_stack("main", index, stack)
end
end
return true
end
local function arrow_step(self, dtime)
self.timer = self.timer + dtime
local pos = self.object:get_pos()
local node = minetest.get_node(pos)
local logging = function(message, level)
minetest.log(level or "action", "[throwing] Arrow "..(self.item or self.name).." throwed by player "..self.player.." "..tostring(self.timer).."s ago "..message)
end
local hit = function(pos, node, obj)
if obj then
if obj:is_player() then
if obj:get_player_name() == self.player then -- Avoid hitting the hitter
return false
end
end
end
local player = minetest.get_player_by_name(self.player)
if not player then -- Possible if the player disconnected
return
end
local function hit_failed()
if not minetest.settings:get_bool("creative_mode") and self.item then
player:get_inventory():add_item("main", self.item)
end
if self.on_hit_fails then
self:on_hit_fails(pos, player, self.data)
end
end
if not self.last_pos then
logging("hitted a node during its first call to the step function")
hit_failed()
return
end
if node and minetest.is_protected(pos, self.player) and not self.allow_protected then -- Forbid hitting nodes in protected areas
minetest.record_protection_violation(pos, self.player)
logging("hitted a node into a protected area")
return
end
if self.on_hit then
local ret, reason = self:on_hit(pos, self.last_pos, node, obj, player, self.data)
if ret == false then
if reason then
logging(": on_hit function failed for reason: "..reason)
else
logging(": on_hit function failed")
end
hit_failed()
return
end
end
if self.on_hit_sound then
minetest.sound_play(self.on_hit_sound, {pos = pos, gain = 0.8})
end
if node then
logging("collided with node "..node.name.." at ("..pos.x..","..pos.y..","..pos.z..")")
elseif obj then
if obj:get_luaentity() then
logging("collided with luaentity "..obj:get_luaentity().name.." at ("..pos.x..","..pos.y..","..pos.z..")")
elseif obj:is_player() then
logging("collided with player "..obj:get_player_name().." at ("..pos.x..","..pos.y..","..pos.z..")")
else
logging("collided with object at ("..pos.x..","..pos.y..","..pos.z..")")
end
end
end
-- Collision with a node
if node.name == "ignore" then
self.object:remove()
logging("reached ignore. Removing.")
return
elseif (minetest.registered_items[node.name] or {}).drawtype ~= "airlike" then
if self.target ~= throwing.target_object then -- throwing.target_both, nil, throwing.target_node, or any invalid value
if hit(pos, node, nil) ~= false then
self.object:remove()
end
else
self.object:remove()
end
return
end
-- Collision with an object
local objs = minetest.get_objects_inside_radius(pos, 1)
for k, obj in pairs(objs) do
if obj:get_luaentity() then
if obj:get_luaentity().name ~= self.name and obj:get_luaentity().name ~= "__builtin:item" then
if self.target ~= throwing.target_node then -- throwing.target_both, nil, throwing.target_object, or any invalid value
if hit(pos, nil, obj) ~= false then
self.object:remove()
end
else
self.object:remove()
end
end
else
if self.target ~= throwing.target_node then -- throwing.target_both, nil, throwing.target_object, or any invalid value
if hit(pos, nil, obj) ~= false then
self.object:remove()
end
else
self.object:remove()
end
end
end
-- Support for shining items using wielded light
if minetest.global_exists("wielded_light") and self.object then
wielded_light.update_light_by_item(self.item, self.object:get_pos())
end
self.last_pos = pos -- Used by the build arrow
end
--[[
on_hit(pos, last_pos, node, object, hitter)
Either node or object is nil, depending whether the arrow collided with an object (luaentity or player) or with a node.
No log message is needed in this function (a generic log message is automatically emitted), except on error or warning.
Should return false or false, reason on failure.
on_throw(pos, hitter)
Unlike on_hit, it is optional.
]]
function throwing.register_arrow(name, def)
throwing.arrows[name] = true
local registration_name = name
if name:sub(1,9) == "throwing:" then
registration_name = ":"..name
end
if not def.groups then
def.groups = {}
end
if not def.groups.dig_immediate then
def.groups.dig_immediate = 3
end
def.inventory_image = def.tiles[1]
def.on_place = function(itemstack, placer, pointed_thing)
if minetest.settings:get_bool("throwing.allow_arrow_placing") and pointed_thing.above then
local playername = placer:get_player_name()
if not minetest.is_protected(pointed_thing.above, playername) then
minetest.log("action", "Player "..playername.." placed arrow "..name.." at ("..pointed_thing.above.x..","..pointed_thing.above.y..","..pointed_thing.above.z..")")
minetest.set_node(pointed_thing.above, {name = name})
itemstack:take_item()
return itemstack
else
minetest.log("warning", "Player "..playername.." tried to place arrow "..name.." into a protected area at ("..pointed_thing.above.x..","..pointed_thing.above.y..","..pointed_thing.above.z..")")
minetest.record_protection_violation(pointed_thing.above, playername)
return itemstack
end
else
return itemstack
end
end
def.drawtype = "nodebox"
def.paramtype = "light"
def.node_box = {
type = "fixed",
fixed = {
-- Shaft
{-6.5/17, -1.5/17, -1.5/17, 6.5/17, 1.5/17, 1.5/17},
-- Spitze
{-4.5/17, 2.5/17, 2.5/17, -3.5/17, -2.5/17, -2.5/17},
{-8.5/17, 0.5/17, 0.5/17, -6.5/17, -0.5/17, -0.5/17},
-- Federn
{6.5/17, 1.5/17, 1.5/17, 7.5/17, 2.5/17, 2.5/17},
{7.5/17, -2.5/17, 2.5/17, 6.5/17, -1.5/17, 1.5/17},
{7.5/17, 2.5/17, -2.5/17, 6.5/17, 1.5/17, -1.5/17},
{6.5/17, -1.5/17, -1.5/17, 7.5/17, -2.5/17, -2.5/17},
{7.5/17, 2.5/17, 2.5/17, 8.5/17, 3.5/17, 3.5/17},
{8.5/17, -3.5/17, 3.5/17, 7.5/17, -2.5/17, 2.5/17},
{8.5/17, 3.5/17, -3.5/17, 7.5/17, 2.5/17, -2.5/17},
{7.5/17, -2.5/17, -2.5/17, 8.5/17, -3.5/17, -3.5/17},
}
}
minetest.register_node(registration_name, def)
minetest.register_entity(registration_name.."_entity", {
physical = false,
visual = "wielditem",
visual_size = {x = 0.125, y = 0.125},
textures = {name},
collisionbox = {0, 0, 0, 0, 0, 0},
on_hit = def.on_hit,
on_hit_sound = def.on_hit_sound,
on_throw_sound = def.on_throw_sound,
on_throw = def.on_throw,
allow_protected = def.allow_protected,
target = def.target,
on_hit_fails = def.on_hit_fails,
on_step = arrow_step,
item = name,
})
end
---------- Bows -----------
function throwing.register_bow(name, def)
if not def.allow_shot then
def.allow_shot = function(player, itemstack, index)
if index >= player:get_inventory():get_size("main") and not def.throw_itself then
return false
end
return throwing.is_arrow(itemstack)
end
end
if not def.inventory_image then
def.inventory_image = def.texture
end
def.on_use = function(itemstack, user, pointed_thing)
-- Cooldown
local meta = itemstack:get_meta()
local cooldown = def.cooldown or tonumber(minetest.settings:get("throwing.bow_cooldown")) or 0.2
if cooldown > 0 and meta:get_int("cooldown") > os.time()
or meta:get_int("delay") > os.time() then
return
end
local bow_index = user:get_wield_index()
local arrow_index = (def.throw_itself and bow_index) or bow_index+1
local res, new_stack = def.allow_shot(user, user:get_inventory():get_stack("main", arrow_index), arrow_index, false)
if not res then
return (def.throw_itself and new_stack) or itemstack
end
-- Sound
if def.sound then
minetest.sound_play(def.sound, {to_player=user:get_player_name()})
end
meta:set_int("delay", os.time() + (def.delay or 0))
minetest.after(def.delay or 0, function()
-- Re-check that the arrow can be thrown. Overwrite the new_stack
local old_new_stack = new_stack
res, new_stack = def.allow_shot(user, user:get_inventory():get_stack("main", arrow_index), arrow_index, true)
if not new_stack then
new_stack = old_new_stack
end
if not res then
return
end
-- Shoot arrow
if shoot_arrow(itemstack, user, bow_index, def.throw_itself, new_stack) then
if not minetest.settings:get_bool("creative_mode") then
itemstack:add_wear(65535 / (def.uses or 50))
end
end
if def.throw_itself then
-- This is a bug. If we return ItemStack(nil), the player punches the entity,
-- and if the entity if a __builtin:item, it gets back to his inventory.
minetest.after(0.1, function()
user:get_inventory():remove_item("main", itemstack)
end)
elseif cooldown > 0 then
meta:set_int("cooldown", os.time() + cooldown)
end
user:get_inventory():set_stack("main", bow_index, itemstack)
end)
return itemstack
end
minetest.register_tool(name, def)
end