Compare commits

...

10 Commits

Author SHA1 Message Date
SmallJoker
dfc724fa06 Ensure Minetest 5.7.0 compatibility
The enforced type checking in Minetest revealed this incorrect API use.
2023-10-18 21:45:32 +02:00
Louis Royer
31f0cf5f86
Add toolranks_extras support (#25)
This displays `bow level x` instead of `tool level x` when toolranks is
enabled, and display `Arrows thrown` instead of `Node dug`.

* Add .luacheckrc and fixes warnings from luacheck
2020-08-14 17:33:20 +02:00
upsilon
4c50e6baec
Fix crash when the bow becomes fully worn
The crash only happened if toolranks was enabled.
2020-05-10 16:36:22 +02:00
upsilon
2d2368529f
Fix a typo causing a crash
Fixes #24
2020-04-27 10:49:00 +02:00
upsilon
49f3f36bdf
Better documentation for new physics in README.md 2020-04-03 08:13:57 +02:00
upsilon
0dd44bb30c
New physics 2020-04-02 17:17:31 +02:00
Louis Royer
bd4d040c4e
Add toolrank optional support (#21)
* Add toolrank support

* Make bow shot able to level up the bow

* Add requested changes

* Change enable_toolranks to no_toolranks

* Remove the overwritting

* no_toolranks in defs in throwing.register_bow

* Toolranks support: minor changes
Reword documentation in README.md, fix a typo in init.lua and enable toolranks even if def.original_description exists---but do not overwrite it.

* Wait until the arrow hits its target before updating bow toolranks uses

* Add a throwing.toolranks settting to allow globally disabling toolranks

Co-authored-by: upsilon <upsilon@langg.net>
2020-04-02 16:11:42 +02:00
upsilon
e2cb1286f1
Add throwing.make_arrow_def back for backwards compatibility, and make arrow_step public 2020-02-18 14:45:30 +01:00
upsilon
edeba9de8b
Change default allow_shot function to allow items that throw itself to be thrown by default
This seems obvious, but wasn't the case
2020-02-16 18:38:49 +01:00
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
6 changed files with 220 additions and 51 deletions

26
.luacheckrc Normal file
View File

@ -0,0 +1,26 @@
std = "lua51+minetest"
unused_args = false
allow_defined_top = true
max_line_length = 999
stds.minetest = {
read_globals = {
"minetest",
"VoxelManip",
"VoxelArea",
"PseudoRandom",
"ItemStack",
"default",
table = {
fields = {
"copy",
},
},
}
}
read_globals = {
"toolranks",
"toolranks_extras",
"wielded_light",
}

View File

@ -13,15 +13,41 @@ Mods based on this API:
The settings are the following: The settings are the following:
``` ```
throwing.velocity_factor = 19 # Trajectory parameters
throwing.horizontal_acceleration_factor = -3
throwing.vertical_acceleration = -10 throwing.vertical_acceleration = -10
throwing.realistic_trajectory = false
throwing.frictional_coefficient = -.5
# How the initial velocity of arrows is computed: simple, strength, or momentum
throwing.velocity_mode = strength
throwing.velocity_factor = 19
# Whether to allow placing an arrow as a node
throwing.allow_arrow_placing = false throwing.allow_arrow_placing = false
# Minimum time between two shots
throwing.bow_cooldown = 0.2 throwing.bow_cooldown = 0.2
# Whether to enable toolranks for bows
throwing.toolranks = true
``` ```
### Trajectory parameters
By default, the trajectory of the arrow is a simple parabola. You can set the vertical acceleration (acceleration of gravity) using `throwing.vertical_acceleration`.
If you want a more realistic trajectory that uses a first-order modelling of air friction, you can set `throwing.realistic_trajectory` to true. In this mode, `throwing.frictional_coefficient` indicates the ratio between the friction force on the arrow and its velocity. It should be negative. The acceleration of the arrow is subsequently determined by dividing the force by the "mass" of the arrow, the reference mass of 1 being the mass of the steel arrow in `throwing_arrows`. Generally, the frictional coefficient should be quite small; a value of -1 will already drastically shorten the range of the arrow. The default is -0.5.
### Initial velocity computation
The mod provides three modes to compute the initial velocity of an arrow after it is shot: simple, strength and momentum.
In simple mode, the initial velocity of the arrow is always the same. The value of this velocity is controlled by the `throwing.velocity_factor` setting, which defaults to 19.
In strength mode (the default), the initial velocity of the arrow only depends on the bow that is used---the more expensive the bow, the faster the arrow. The bow strength is multiplied by the velocity factor to compute the speed of the arrow. For reference, the steel bow in `throwing_arrows` has a strength of about 1.
Finally, momentum mode is the most realistic. It computes the velocity of the arrow based on the bow strength, as in the strength mode, and on the mass of the arrow: the heavier the arrow, the slower it will be shot. It is called the momentum mode because, in this mode, the strength of a bow indicates the initial momentum of the arrow rather than its initial speed.
## API ## API
There are two available functions in the mod API: There are two available functions in the mod API:
@ -49,6 +75,8 @@ Definition: definition table, containing:
* function spawn_arrow_entity(position, arrow, player): defaults to throwing.spawn_arrow_entity * function spawn_arrow_entity(position, arrow, player): defaults to throwing.spawn_arrow_entity
* sound: sound to be played when the bow is used * sound: sound to be played when the bow is used
* delay: delay before throwing the arrow * delay: delay before throwing the arrow
* no_toolranks: If true, toolranks support is disabled for this item. Defaults to false.
* strength: strength of the bow, see above. Defaults to 1.
]] ]]
-- Example: -- Example:
@ -66,6 +94,7 @@ Definition: definition table, containing:
* tiles (essential): tiles of the arrow. * tiles (essential): tiles of the arrow.
* target (optional, defaulting to throwing.target_both): what the arrow is able to hit (throwing.target_node, throwing.target_object, throwing.target_both). * target (optional, defaulting to throwing.target_both): what the arrow is able to hit (throwing.target_node, throwing.target_object, throwing.target_both).
* allow_protected (optional, defaulting to false): whether the arrow can be throw in a protected area * allow_protected (optional, defaulting to false): whether the arrow can be throw in a protected area
* mass (optional, defaulting to 1): the mass of the arrow (see above)
* on_hit_sound (optional): sound played when the arrow hits a node or an object. * on_hit_sound (optional): sound played when the arrow hits a node or an object.
* on_hit(self, pos, last_pos, node, object, hitter, data) (optional but very useful): callback function: * on_hit(self, pos, last_pos, node, object, hitter, data) (optional but very useful): callback function:
- pos: the position of the hit node or object. - pos: the position of the hit node or object.

196
init.lua
View File

@ -7,6 +7,8 @@ throwing.target_node = 2
throwing.target_both = 3 throwing.target_both = 3
throwing.modname = minetest.get_current_modname() throwing.modname = minetest.get_current_modname()
local S = minetest.get_translator("throwing")
local use_toolranks = minetest.get_modpath("toolranks") and minetest.settings:get_bool("throwing.toolranks", true)
--------- Arrows functions --------- --------- Arrows functions ---------
function throwing.is_arrow(itemstack) function throwing.is_arrow(itemstack)
@ -23,65 +25,103 @@ function throwing.spawn_arrow_entity(pos, arrow, player)
return minetest.registered_items[arrow].throwing_entity(pos, player) return minetest.registered_items[arrow].throwing_entity(pos, player)
end end
else else
obj = minetest.add_entity(pos, "__builtin:item", arrow) return minetest.add_entity(pos, "__builtin:item", arrow)
end end
end end
local function shoot_arrow(itemstack, player, index, throw_itself, new_stack) local function apply_realistic_acceleration(obj, mass)
if not minetest.settings:get_bool("throwing.realistic_trajectory", false) then
return
end
local vertical_acceleration = tonumber(minetest.settings:get("throwing.vertical_acceleration")) or -10
local friction_coef = tonumber(minetest.settings:get("throwing.frictional_coefficient")) or -3
local velocity = obj:get_velocity()
obj:set_acceleration({
x = friction_coef * velocity.x / mass,
y = friction_coef * velocity.y / mass + vertical_acceleration,
z = friction_coef * velocity.z / mass
})
end
local function shoot_arrow(def, toolranks_data, player, bow_index, throw_itself, new_stack)
local inventory = player:get_inventory() local inventory = player:get_inventory()
if not throw_itself then local arrow_index
if index >= player:get_inventory():get_size("main") then if throw_itself then
arrow_index = bow_index
else
if bow_index >= player:get_inventory():get_size("main") then
return false return false
end end
index = index + 1 arrow_index = bow_index + 1
end end
local arrow_stack = inventory:get_stack("main", index) local arrow_stack = inventory:get_stack("main", arrow_index)
local arrow = arrow_stack:get_name() local arrow = arrow_stack:get_name()
local playerpos = player:get_pos() local playerpos = player:get_pos()
local pos = {x=playerpos.x,y=playerpos.y+1.5,z=playerpos.z} 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 obj = (def.spawn_arrow_entity or throwing.spawn_arrow_entity)(pos, arrow, player)
local luaentity = obj:get_luaentity() local luaentity = obj:get_luaentity()
-- Set custom data in the entity
luaentity.player = player:get_player_name() luaentity.player = player:get_player_name()
if not luaentity.item then if not luaentity.item then
luaentity.item = arrow luaentity.item = arrow
end end
luaentity.data = {}
luaentity.timer = 0
luaentity.toolranks = toolranks_data -- May be nil if toolranks is disabled
if luaentity.on_throw then if luaentity.on_throw then
if luaentity:on_throw(pos, player, arrow_stack, index, luaentity.data) == false then if luaentity:on_throw(pos, player, arrow_stack, arrow_index, luaentity.data) == false then
obj:remove() obj:remove()
return false return false
end end
end end
local dir = player:get_look_dir() 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 local vertical_acceleration = tonumber(minetest.settings:get("throwing.vertical_acceleration")) or -10
local velocity_factor = tonumber(minetest.settings:get("throwing.velocity_factor")) or 19
local velocity_mode = minetest.settings:get("throwing.velocity_mode") or "strength"
obj:set_velocity({x=dir.x*velocity_factor, y=dir.y*velocity_factor, z=dir.z*velocity_factor}) local velocity
obj:set_acceleration({x=dir.x*horizontal_acceleration_factor, y=vertical_acceleration, z=dir.z*horizontal_acceleration_factor}) if velocity_mode == "simple" then
velocity = velocity_factor
elseif velocity_mode == "momentum" then
velocity = def.strength * velocity_factor / luaentity.mass
else
velocity = def.strength * velocity_factor
end
obj:set_velocity({
x = dir.x * velocity,
y = dir.y * velocity,
z = dir.z * velocity
})
obj:set_acceleration({x = 0, y = vertical_acceleration, z = 0})
obj:set_yaw(player:get_look_horizontal()-math.pi/2) obj:set_yaw(player:get_look_horizontal()-math.pi/2)
apply_realistic_acceleration(obj, luaentity.mass)
if luaentity.on_throw_sound ~= "" then if luaentity.on_throw_sound ~= "" then
minetest.sound_play(luaentity.on_throw_sound or "throwing_sound", {pos=playerpos, gain = 0.5}) minetest.sound_play(luaentity.on_throw_sound or "throwing_sound", {pos=playerpos, gain = 0.5})
end end
if not minetest.settings:get_bool("creative_mode") then if not minetest.settings:get_bool("creative_mode") then
if new_stack then inventory:set_stack("main", arrow_index, new_stack)
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 end
return true return true
end end
local function arrow_step(self, dtime) function throwing.arrow_step(self, dtime)
if not self.timer or not self.player then
self.object:remove()
return
end
self.timer = self.timer + dtime self.timer = self.timer + dtime
local pos = self.object:get_pos() local pos = self.object:get_pos()
local node = minetest.get_node(pos) local node = minetest.get_node(pos)
@ -90,7 +130,7 @@ local function arrow_step(self, dtime)
minetest.log(level or "action", "[throwing] Arrow "..(self.item or self.name).." throwed by player "..self.player.." "..tostring(self.timer).."s ago "..message) minetest.log(level or "action", "[throwing] Arrow "..(self.item or self.name).." throwed by player "..self.player.." "..tostring(self.timer).."s ago "..message)
end end
local hit = function(pos, node, obj) local hit = function(pos1, node1, obj)
if obj then if obj then
if obj:is_player() then if obj:is_player() then
if obj:get_player_name() == self.player then -- Avoid hitting the hitter if obj:get_player_name() == self.player then -- Avoid hitting the hitter
@ -109,7 +149,7 @@ local function arrow_step(self, dtime)
player:get_inventory():add_item("main", self.item) player:get_inventory():add_item("main", self.item)
end end
if self.on_hit_fails then if self.on_hit_fails then
self:on_hit_fails(pos, player, self.data) self:on_hit_fails(pos1, player, self.data)
end end
end end
@ -119,14 +159,14 @@ local function arrow_step(self, dtime)
return return
end end
if node and minetest.is_protected(pos, self.player) and not self.allow_protected then -- Forbid hitting nodes in protected areas if node1 and minetest.is_protected(pos1, self.player) and not self.allow_protected then -- Forbid hitting nodes in protected areas
minetest.record_protection_violation(pos, self.player) minetest.record_protection_violation(pos1, self.player)
logging("hitted a node into a protected area") logging("hitted a node into a protected area")
return return
end end
if self.on_hit then if self.on_hit then
local ret, reason = self:on_hit(pos, self.last_pos, node, obj, player, self.data) local ret, reason = self:on_hit(pos1, self.last_pos, node1, obj, player, self.data)
if ret == false then if ret == false then
if reason then if reason then
logging(": on_hit function failed for reason: "..reason) logging(": on_hit function failed for reason: "..reason)
@ -140,17 +180,33 @@ local function arrow_step(self, dtime)
end end
if self.on_hit_sound then if self.on_hit_sound then
minetest.sound_play(self.on_hit_sound, {pos = pos, gain = 0.8}) minetest.sound_play(self.on_hit_sound, {pos = pos1, gain = 0.8})
end end
if node then
logging("collided with node "..node.name.." at ("..pos.x..","..pos.y..","..pos.z..")") local identifier
if node1 then
identifier = "node " .. node1.name
elseif obj then elseif obj then
if obj:get_luaentity() then if obj:get_luaentity() then
logging("collided with luaentity "..obj:get_luaentity().name.." at ("..pos.x..","..pos.y..","..pos.z..")") identifier = "luaentity " .. obj:get_luaentity().name
elseif obj:is_player() then elseif obj:is_player() then
logging("collided with player "..obj:get_player_name().." at ("..pos.x..","..pos.y..","..pos.z..")") identifier = "player " .. obj:get_player_name()
else else
logging("collided with object at ("..pos.x..","..pos.y..","..pos.z..")") identifier = "unknown object"
end
end
if identifier then
logging("collided with " .. identifier .. " at " .. minetest.pos_to_string(pos1) .. ")")
end
-- Toolranks support: update bow uses
if self.toolranks then
local inventory = player:get_inventory()
-- Check that the player did not move the bow
local current_stack = inventory:get_stack("main", self.toolranks.index)
if current_stack:get_name() == self.toolranks.name then
local new_itemstack = toolranks.new_afteruse(current_stack, player, nil, {wear = self.toolranks.wear})
inventory:set_stack("main", self.toolranks.index, new_itemstack)
end end
end end
end end
@ -200,14 +256,14 @@ local function arrow_step(self, dtime)
wielded_light.update_light_by_item(self.item, self.object:get_pos()) wielded_light.update_light_by_item(self.item, self.object:get_pos())
end end
apply_realistic_acceleration(self.object, self.mass) -- Physics: air friction
self.last_pos = pos -- Used by the build arrow self.last_pos = pos -- Used by the build arrow
end end
-- Backwards compatibility
function throwing.make_arrow_def(def) function throwing.make_arrow_def(def)
def.timer = 0 def.on_step = throwing.arrow_step
def.player = ""
def.on_step = arrow_step
def.data = {}
return def return def
end end
@ -234,17 +290,18 @@ function throwing.register_arrow(name, def)
if not def.groups.dig_immediate then if not def.groups.dig_immediate then
def.groups.dig_immediate = 3 def.groups.dig_immediate = 3
end end
def.inventory_image = def.tiles[1] def.inventory_image = def.tiles[1]
def.on_place = function(itemstack, placer, pointed_thing) def.on_place = function(itemstack, placer, pointed_thing)
if minetest.settings:get_bool("throwing.allow_arrow_placing") and pointed_thing.above then if minetest.settings:get_bool("throwing.allow_arrow_placing") and pointed_thing.above then
local playername = placer:get_player_name() local playername = placer:get_player_name()
if not minetest.is_protected(pointed_thing.above, playername) then 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.log("action", "Player "..playername.." placed arrow "..name.." at "..minetest.pos_to_string(pointed_thing.above))
minetest.set_node(pointed_thing.above, {name = name}) minetest.set_node(pointed_thing.above, {name = name})
itemstack:take_item() itemstack:take_item()
return itemstack return itemstack
else 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.log("warning", "Player "..playername.." tried to place arrow "..name.." into a protected area at "..minetest.pos_to_string(pointed_thing.above))
minetest.record_protection_violation(pointed_thing.above, playername) minetest.record_protection_violation(pointed_thing.above, playername)
return itemstack return itemstack
end end
@ -276,7 +333,7 @@ function throwing.register_arrow(name, def)
} }
minetest.register_node(registration_name, def) minetest.register_node(registration_name, def)
minetest.register_entity(registration_name.."_entity", throwing.make_arrow_def{ minetest.register_entity(registration_name.."_entity", {
physical = false, physical = false,
visual = "wielditem", visual = "wielditem",
visual_size = {x = 0.125, y = 0.125}, visual_size = {x = 0.125, y = 0.125},
@ -289,24 +346,44 @@ function throwing.register_arrow(name, def)
allow_protected = def.allow_protected, allow_protected = def.allow_protected,
target = def.target, target = def.target,
on_hit_fails = def.on_hit_fails, on_hit_fails = def.on_hit_fails,
on_step = throwing.arrow_step,
item = name, item = name,
mass = def.mass or 1,
}) })
end end
---------- Bows ----------- ---------- Bows -----------
if use_toolranks and minetest.get_modpath("toolranks_extras") and toolranks_extras.register_tool_type then
toolranks_extras.register_tool_type("bow", S("bow"), S("Arrows thrown"))
end
function throwing.register_bow(name, def) function throwing.register_bow(name, def)
local enable_toolranks = use_toolranks and not def.no_toolranks
def.name = name
if not def.allow_shot then if not def.allow_shot then
def.allow_shot = function(player, itemstack, index) def.allow_shot = function(player, itemstack, index)
if index >= player:get_inventory():get_size("main") and not def.throw_itself then if index >= player:get_inventory():get_size("main") and not def.throw_itself then
return false return false
end end
return throwing.is_arrow(itemstack) return throwing.is_arrow(itemstack) or def.throw_itself
end end
end end
if not def.inventory_image then if not def.inventory_image then
def.inventory_image = def.texture def.inventory_image = def.texture
end end
if not def.strength then
def.strength = 20
end
local throw_sound = def.sound
-- Reserved field by Minetest. Prevent leaking irrelevant data to the registration API
def.sound = nil
def.on_use = function(itemstack, user, pointed_thing) def.on_use = function(itemstack, user, pointed_thing)
-- Cooldown -- Cooldown
local meta = itemstack:get_meta() local meta = itemstack:get_meta()
@ -325,33 +402,50 @@ function throwing.register_bow(name, def)
end end
-- Sound -- Sound
if def.sound then if throw_sound then
minetest.sound_play(def.sound, {to_player=user:get_player_name()}) minetest.sound_play(throw_sound, {to_player=user:get_player_name()})
end end
meta:set_int("delay", os.time() + (def.delay or 0)) meta:set_int("delay", os.time() + (def.delay or 0))
minetest.after(def.delay or 0, function() minetest.after(def.delay or 0, function()
-- Re-check that the arrow can be thrown. Overwrite the new_stack -- Re-check that the arrow can be thrown. Overwrite the new_stack
local old_new_stack = 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 local arrow_stack = user:get_inventory():get_stack("main", arrow_index)
new_stack = old_new_stack
end res, new_stack = def.allow_shot(user, arrow_stack, arrow_index, true)
if not res then if not res then
return return
end end
if not new_stack then
new_stack = old_new_stack
end
if not new_stack then
arrow_stack:take_item()
new_stack = arrow_stack
end
-- Shoot arrow -- Shoot arrow
if shoot_arrow(itemstack, user, bow_index, def.throw_itself, new_stack) then local uses = 65535 / (def.uses or 50)
local toolranks_data
if enable_toolranks then
toolranks_data = {
name = itemstack:get_name(),
index = bow_index,
wear = uses
}
end
if shoot_arrow(def, toolranks_data, user, bow_index, def.throw_itself, new_stack) then
if not minetest.settings:get_bool("creative_mode") then if not minetest.settings:get_bool("creative_mode") then
itemstack:add_wear(65535 / (def.uses or 50)) itemstack:add_wear(uses)
end end
end end
if def.throw_itself then if def.throw_itself then
-- This is a bug. If we return ItemStack(nil), the player punches the entity, -- 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. -- and if the entity is a __builtin:item, it gets back to his inventory.
minetest.after(0.1, function() minetest.after(0.1, function()
user:get_inventory():remove_item("main", itemstack) user:get_inventory():remove_item("main", itemstack)
end) end)
@ -362,5 +456,11 @@ function throwing.register_bow(name, def)
end) end)
return itemstack return itemstack
end end
if enable_toolranks then
def.original_description = def.original_description or def.description
def.description = toolranks.create_description(def.description)
end
minetest.register_tool(name, def) minetest.register_tool(name, def)
end end

7
locale/template.txt Normal file
View File

@ -0,0 +1,7 @@
# textdomain: throwing
### init.lua ###
Arrows thrown=
bow=

7
locale/throwing.fr.tr Normal file
View File

@ -0,0 +1,7 @@
# textdomain: throwing
### init.lua ###
Arrows thrown=Flèches lancées
bow=arc

View File

@ -1,3 +1,3 @@
name = throwing name = throwing
optional_depends = wielded_light optional_depends = wielded_light, toolranks, toolranks_extras
description = Throwing library: provides an API for registering throwing and throwable things description = Throwing library: provides an API for registering throwing and throwable things