Compare commits

...

76 Commits

Author SHA1 Message Date
teknomunk 22980c2951 Replace _puncher with _owner, rework projectile code to make _owner a string, copy mcl_util.gen_uuid() from minecart branch, add mcl_util.get_entity_id(), fix crash 2024-10-24 06:39:37 -05:00
teknomunk c5e057cb6b Remove debug dump, move maximum time to live to vl_projectile 2024-10-20 21:01:33 -05:00
teknomunk 4893f1711a Document vl_projectile.replace_with_item_drop 2024-10-20 20:47:47 -05:00
teknomunk ea229e8dbc Move projectile stick logic into vl_projectile 2024-10-20 15:42:00 -05:00
teknomunk baa5fb5952 Move tracer to vl_projectile 2024-10-20 13:31:29 -05:00
teknomunk 53a34747da Change mcl_bows to use standard vl_projectile on_step handler, move burning behavior to vl_projectile 2024-10-20 13:15:47 -05:00
teknomunk b8a115753e Move arrow-node logic out of mcl_bows/arrow.lua and into the node definitions under _vl_projectile.on_collide 2024-10-20 12:36:11 -05:00
teknomunk 549693ff97 Remove secondary collision check in mcl_bows/arrow.lua (it was just hitting grass) 2024-10-20 07:18:59 -05:00
teknomunk 4f9faffcb3 Remove code with no effect other than emitting a warning 2024-10-11 14:07:37 -05:00
teknomunk d878c57531 Resolve luacheck findings 2024-10-11 11:24:02 +02:00
teknomunk d0dd880c51 Fix crashes in mcl_throwing 2024-10-11 11:24:02 +02:00
teknomunk cf3354ef79 Fix crash when hit with an arrow that is not piercing 2024-10-11 11:24:02 +02:00
the-real-herowl 6af77081a7 Rockets and piercing arrows fixed
They both work properly now.
2024-10-11 11:24:02 +02:00
the-real-herowl 7bb60dfeeb Fix throwables' punch allow check 2024-10-11 11:24:02 +02:00
the-real-herowl ca095395a9 Fixed abnormal skull rotation 2024-10-11 11:24:02 +02:00
the-real-herowl 12f2147372 Fixed wither skulls
- wither skulls now hit players again
- wither skulls (strong version) don't cause a crash anymore
2024-10-11 11:24:02 +02:00
teknomunk b9784fdba1 Add attribution with exact links for minetest code 2024-10-11 11:24:02 +02:00
teknomunk a666739d80 Add hook to allow piercing enchantment to be implemented 2024-10-11 11:24:02 +02:00
teknomunk 08646b1d13 Fix chick spawning in the presence of a functional has_room() check 2024-10-11 11:24:02 +02:00
teknomunk 1c4cf2f17c Fix dispenser arrows so they hit players and entities, minetest.register_entity -> vl_projectile.register for mcl_throwing:egg 2024-10-11 11:24:02 +02:00
teknomunk 7bea6215d0 Make egg and snowball stack count match master 2024-10-11 11:24:02 +02:00
teknomunk 348b13764f Prevent dereferencing nil in wither.lua, fix minor typo in debug statement 2024-10-11 11:24:02 +02:00
teknomunk 5f8c378002 Add workarround for random velocity change of particles on creation 2024-10-11 11:24:02 +02:00
teknomunk 65e25391db Make sure a projectile sets on fire the thing it collided with 2024-10-11 11:24:02 +02:00
teknomunk ead59d4ead Stop wither from shooting itself 2024-10-11 11:24:02 +02:00
teknomunk 049306ecc6 Fix player-mcl_throwing collisions, fix chick spawning on egg collisions, luacheck fixes 2024-10-11 11:24:02 +02:00
teknomunk cd6da3dcf2 Fix unintentional collision box change 2024-10-11 11:24:02 +02:00
teknomunk 7bdfcdc9dd Fix luacheck findings for this PR: remove unused variables, remove overwritten value, add missing dependencies, fix undefined variable usage 2024-10-11 11:24:02 +02:00
teknomunk aebafd7af0 Prevent projectiles from crashing server when removed when it punched something (wither skull hitting the wither will cause this), mark wither skull with _removed = true when death timer removes it 2024-10-11 11:24:02 +02:00
teknomunk 4a71d993d3 Allow mob projectiles to hit their owner after 2 seconds (used by ghast fireball) 2024-10-11 11:24:02 +02:00
teknomunk aa5a696341 Make snowballs have the same collision conditions as before 2024-10-11 11:24:02 +02:00
teknomunk 8d570958d4 Undo conversions to use mcl_bows:arrow_entity and use arrow_item.."_entity" like master 2024-10-11 11:24:02 +02:00
teknomunk 74882d2bbf Convert flying bobber to vl_projectile, modify mcl_throwing.register_throwable_object() to check for _vl_projectile field 2024-10-11 11:24:02 +02:00
teknomunk b904204c78 Remove redundant check 2024-10-11 11:24:02 +02:00
teknomunk 1cceb7378d Correct behavior when ignore_gravity flag is true, fix mob projectile velocity calculation, add missing entity_def lookup 2024-10-11 11:24:02 +02:00
teknomunk c8512d29bd Convert mcl_mobs.register_arrow() to use vl_projectile, tested only with shulker bullet so far 2024-10-11 11:24:02 +02:00
teknomunk 39e93b7a54 Restore tipped arrow entity registration as near copy of mcl_bows:arrow_entity 2024-10-11 11:24:02 +02:00
teknomunk d187b3bf55 Small optimization for calculating _allow_punch 2024-10-11 11:24:02 +02:00
teknomunk ed373e9f37 Remove unnecessary vectory copy 2024-10-11 11:24:02 +02:00
teknomunk 9f9c590c8d Make splash, lingering and othe projectiles hit players 2024-10-11 11:24:02 +02:00
teknomunk ca63f1ce58 Stop minimum draw arrows from hitting player that shot them 2024-10-11 11:24:02 +02:00
teknomunk 52946b3a26 Prevent collisions with entities until projectile is at least one node from where it started (to prevent always hitting yourself), modify mcl_bows.shoot_arrow() and mcl_bows_s.shoot_arrow_crossbow() to use vl_projectile.create(), fix projectiles damaging players 2024-10-11 11:24:02 +02:00
teknomunk 6d838e7fbd Inline dir_to_pitch() and use correct formula for pitch 2024-10-11 11:24:02 +02:00
teknomunk 36d20e6f48 Address most of kno10's comments about mcl_utils/node.lua and a couple others, fix lingering potion sound 2024-10-11 11:24:02 +02:00
teknomunk adb6e22ce5 Address more review comments 2024-10-11 11:24:02 +02:00
teknomunk 29bfd6ce21 Make arrows damage players, update API documentation, fix several crashes that occurred when arrows hit a player 2024-10-11 11:24:02 +02:00
teknomunk e212f3dd08 Fix picking up tipped arrows 2024-10-11 11:24:02 +02:00
teknomunk 0b500462ab Fix crash with splash potions of harming, make splash particles match potion color 2024-10-11 11:24:02 +02:00
teknomunk 55c8f4cb84 Fix splash potions, fix crash when projectiles hit unknown nodes 2024-10-11 11:24:02 +02:00
teknomunk 227bbebe3b Remove logging in tipped_arrows, prevent crash when spawning chicks, add extra safety check in vl_projectile.collides_with_solids 2024-10-11 11:24:02 +02:00
teknomunk 26b858c463 Lingering potions should not collide with liquids, change behaviors assersion loop to catch nil members 2024-10-11 11:24:02 +02:00
teknomunk ca8f7eb700 Convert lingering potion to vl_projectile 2024-10-11 11:24:02 +02:00
teknomunk eca43272bb Convert splash potion to vl_projectile, remove dead code, precompute several values in splash potion code, add check of behaviors array in vl_projectile 2024-10-11 11:24:02 +02:00
teknomunk a7342eb024 Fix crash related to nil _time_in_air 2024-10-11 11:24:02 +02:00
teknomunk 5e024bf57f Revert entity->object for three lines 2024-10-11 11:24:02 +02:00
teknomunk 05be7b0922 Fix typo _ -> . 2024-10-11 11:24:02 +02:00
teknomunk 1b824cebb3 Add empty line 2024-10-11 11:24:02 +02:00
teknomunk 436033eba7 Fix typo in api.md, add TODO, change entity -> object 2024-10-11 11:24:02 +02:00
teknomunk 52d2c4a26e Fix typos documentation, add ignore_gravity and liquid_drag options, use vl_projectile.register() for enter pearl, move projectile physics to vl_projectile add hook for future vl_physics support 2024-10-11 11:24:02 +02:00
teknomunk 5bd1bcd425 Update mcl_util.get_double_container_neighbor_pos() to use vector.offset 2024-10-11 11:24:02 +02:00
teknomunk 16ddcf99eb Resolve more review comments, fix crash 2024-10-11 11:24:02 +02:00
teknomunk 3b0690dcc3 Resolve review comments 2024-10-11 11:24:02 +02:00
teknomunk 52381a1f58 Remove debug print 2024-10-11 11:24:02 +02:00
teknomunk 149e69452e Give arrows a maximum lifetime to prevent them from getting stuck in mid-air forever 2024-10-11 11:24:02 +02:00
teknomunk f55f647f2e Fix documentation for raycast_collides_with_entities 2024-10-11 11:24:02 +02:00
teknomunk 1ee97a2e73 Add API documentation, minor API cleanup 2024-10-11 11:24:02 +02:00
teknomunk 6eab0433ab Convert rocket to use vl_projectile API using mcl_bows:arrow as a template, remove some dead code 2024-10-11 11:24:02 +02:00
teknomunk 1d153f4f2a Fold tipped arrows into standard arrows and handle effects via item definition callback, remove arrows when they collide with entities, update handling of removing projectiles 2024-10-11 11:24:02 +02:00
teknomunk d35246135b Fix crash on startup 2024-10-11 11:24:02 +02:00
teknomunk 0140d1b81b More refactoring to generalize arrow code into projectile 2024-10-11 11:24:02 +02:00
teknomunk f43bbdd78b Start mcl_bows:arrow_entity refactor 2024-10-11 11:24:02 +02:00
teknomunk d32d59cde2 Remove _thrower from vl_projectile (uses self._vl_projectile.owner instead), reorder some code 2024-10-11 11:24:02 +02:00
teknomunk 01d3159dd3 Finish reworking all items in mcl_throwing 2024-10-11 11:24:02 +02:00
teknomunk b1b3b082a1 Add in vl_projectile, add mcl_util.match_node_to_filter 2024-10-11 11:24:02 +02:00
teknomunk a1ebc2c040 Move node related functions to mods/CORE/mcl_util/nodes.lua 2024-10-11 11:24:02 +02:00
teknomunk 1c07325c76 Create vl_projectile and refactor snowball and partially refactor ender pearl 2024-10-11 11:24:02 +02:00
31 changed files with 2061 additions and 2466 deletions

View File

@ -1,6 +1,9 @@
mcl_util = {} mcl_util = {}
dofile(minetest.get_modpath(minetest.get_current_modname()).."/roman_numerals.lua") local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
dofile(modpath.."/roman_numerals.lua")
dofile(modpath.."/nodes.lua")
-- Updates all values in t using values from to*. -- Updates all values in t using values from to*.
function table.update(t, ...) function table.update(t, ...)
@ -126,24 +129,6 @@ function mcl_util.validate_vector (vect)
return false return false
end end
-- Minetest 5.3.0 or less can only measure the light level. This came in at 5.4
-- This function has been known to fail in multiple places so the error handling is added increase safety and improve
-- debugging. See:
-- https://git.minetest.land/VoxeLibre/VoxeLibre/issues/1392
function mcl_util.get_natural_light (pos, time)
local status, retVal = pcall(minetest.get_natural_light, pos, time)
if status then
return retVal
else
minetest.log("warning", "Failed to get natural light at pos: " .. dump(pos) .. ", time: " .. dump(time))
if (pos) then
local node = minetest.get_node(pos)
minetest.log("warning", "Node at pos: " .. dump(node.name))
end
end
return 0
end
function mcl_util.file_exists(name) function mcl_util.file_exists(name)
if type(name) ~= "string" then return end if type(name) ~= "string" then return end
local f = io.open(name) local f = io.open(name)
@ -154,119 +139,6 @@ function mcl_util.file_exists(name)
return true return true
end end
-- Based on minetest.rotate_and_place
--[[
Attempt to predict the desired orientation of the pillar-like node
defined by `itemstack`, and place it accordingly in one of 3 possible
orientations (X, Y or Z).
Stacks are handled normally if the `infinitestacks`
field is false or omitted (else, the itemstack is not changed).
* `invert_wall`: if `true`, place wall-orientation on the ground and ground-
orientation on wall
This function is a simplified version of minetest.rotate_and_place.
The Minetest function is seen as inappropriate because this includes mirror
images of possible orientations, causing problems with pillar shadings.
]]
function mcl_util.rotate_axis_and_place(itemstack, placer, pointed_thing, infinitestacks, invert_wall)
local unode = minetest.get_node_or_nil(pointed_thing.under)
if not unode then
return
end
local undef = minetest.registered_nodes[unode.name]
if undef and undef.on_rightclick and not invert_wall then
undef.on_rightclick(pointed_thing.under, unode, placer,
itemstack, pointed_thing)
return
end
local fdir = minetest.dir_to_facedir(placer:get_look_dir())
local wield_name = itemstack:get_name()
local above = pointed_thing.above
local under = pointed_thing.under
local is_x = (above.x ~= under.x)
local is_y = (above.y ~= under.y)
local is_z = (above.z ~= under.z)
local anode = minetest.get_node_or_nil(above)
if not anode then
return
end
local pos = pointed_thing.above
local node = anode
if undef and undef.buildable_to then
pos = pointed_thing.under
node = unode
end
if minetest.is_protected(pos, placer:get_player_name()) then
minetest.record_protection_violation(pos, placer:get_player_name())
return
end
local ndef = minetest.registered_nodes[node.name]
if not ndef or not ndef.buildable_to then
return
end
local p2
if is_y then
p2 = 0
elseif is_x then
p2 = 12
elseif is_z then
p2 = 6
end
minetest.set_node(pos, {name = wield_name, param2 = p2})
if not infinitestacks then
itemstack:take_item()
return itemstack
end
end
-- Wrapper of above function for use as `on_place` callback (Recommended).
-- Similar to minetest.rotate_node.
function mcl_util.rotate_axis(itemstack, placer, pointed_thing)
mcl_util.rotate_axis_and_place(itemstack, placer, pointed_thing,
minetest.is_creative_enabled(placer:get_player_name()),
placer:get_player_control().sneak)
return itemstack
end
-- Returns position of the neighbor of a double chest node
-- or nil if node is invalid.
-- This function assumes that the large chest is actually intact
-- * pos: Position of the node to investigate
-- * param2: param2 of that node
-- * side: Which "half" the investigated node is. "left" or "right"
function mcl_util.get_double_container_neighbor_pos(pos, param2, side)
if side == "right" then
if param2 == 0 then
return {x = pos.x - 1, y = pos.y, z = pos.z}
elseif param2 == 1 then
return {x = pos.x, y = pos.y, z = pos.z + 1}
elseif param2 == 2 then
return {x = pos.x + 1, y = pos.y, z = pos.z}
elseif param2 == 3 then
return {x = pos.x, y = pos.y, z = pos.z - 1}
end
else
if param2 == 0 then
return {x = pos.x + 1, y = pos.y, z = pos.z}
elseif param2 == 1 then
return {x = pos.x, y = pos.y, z = pos.z - 1}
elseif param2 == 2 then
return {x = pos.x - 1, y = pos.y, z = pos.z}
elseif param2 == 3 then
return {x = pos.x, y = pos.y, z = pos.z + 1}
end
end
end
--- Selects item stack to transfer from --- Selects item stack to transfer from
---@param src_inventory InvRef Source innentory to pull from ---@param src_inventory InvRef Source innentory to pull from
---@param src_list string Name of source inventory list to pull from ---@param src_list string Name of source inventory list to pull from
@ -424,61 +296,6 @@ function mcl_util.is_fuel(item)
return minetest.get_craft_result({method = "fuel", width = 1, items = {item}}).time ~= 0 return minetest.get_craft_result({method = "fuel", width = 1, items = {item}}).time ~= 0
end end
-- Returns a on_place function for plants
-- * condition: function(pos, node, itemstack)
-- * A function which is called by the on_place function to check if the node can be placed
-- * Must return true, if placement is allowed, false otherwise.
-- * If it returns a string, placement is allowed, but will place this itemstring as a node instead
-- * pos, node: Position and node table of plant node
-- * itemstack: Itemstack to place
function mcl_util.generate_on_place_plant_function(condition)
return function(itemstack, placer, pointed_thing)
if pointed_thing.type ~= "node" then
-- no interaction possible with entities
return itemstack
end
-- Call on_rightclick if the pointed node defines it
local node = minetest.get_node(pointed_thing.under)
if placer and not placer:get_player_control().sneak then
if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then
return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
end
end
local place_pos
local def_under = minetest.registered_nodes[minetest.get_node(pointed_thing.under).name]
local def_above = minetest.registered_nodes[minetest.get_node(pointed_thing.above).name]
if not def_under or not def_above then
return itemstack
end
if def_under.buildable_to and def_under.name ~= itemstack:get_name() then
place_pos = pointed_thing.under
elseif def_above.buildable_to and def_above.name ~= itemstack:get_name() then
place_pos = pointed_thing.above
pointed_thing.under = pointed_thing.above
else
return itemstack
end
-- Check placement rules
local result, param2 = condition(place_pos, node, itemstack)
if result == true then
local idef = itemstack:get_definition()
local new_itemstack, success = minetest.item_place_node(itemstack, placer, pointed_thing, param2)
if success then
if idef.sounds and idef.sounds.place then
minetest.sound_play(idef.sounds.place, {pos = pointed_thing.above, gain = 1}, true)
end
end
itemstack = new_itemstack
end
return itemstack
end
end
-- adjust the y level of an object to the center of its collisionbox -- adjust the y level of an object to the center of its collisionbox
-- used to get the origin position of entity explosions -- used to get the origin position of entity explosions
function mcl_util.get_object_center(obj) function mcl_util.get_object_center(obj)
@ -750,243 +567,6 @@ function mcl_util.set_bone_position(obj, bone, pos, rot)
end end
end end
---Return a function to use in `on_place`.
---
---Allow to bypass the `buildable_to` node field in a `on_place` callback.
---
---You have to make sure that the nodes you return true for have `buildable_to = true`.
---@param func fun(node_name: string): boolean Return `true` if node must not replace the buildable_to node which have `node_name`
---@return fun(itemstack: ItemStack, placer: ObjectRef, pointed_thing: pointed_thing, param2: integer): ItemStack?
function mcl_util.bypass_buildable_to(func)
--------------------------
-- MINETEST CODE: UTILS --
--------------------------
local function copy_pointed_thing(pointed_thing)
return {
type = pointed_thing.type,
above = pointed_thing.above and vector.copy(pointed_thing.above),
under = pointed_thing.under and vector.copy(pointed_thing.under),
ref = pointed_thing.ref,
}
end
local function user_name(user)
return user and user:get_player_name() or ""
end
-- Returns a logging function. For empty names, does not log.
local function make_log(name)
return name ~= "" and minetest.log or function() end
end
local function check_attached_node(p, n, group_rating)
local def = core.registered_nodes[n.name]
local d = vector.zero()
if group_rating == 3 then
-- always attach to floor
d.y = -1
elseif group_rating == 4 then
-- always attach to ceiling
d.y = 1
elseif group_rating == 2 then
-- attach to facedir or 4dir direction
if (def.paramtype2 == "facedir" or
def.paramtype2 == "colorfacedir") then
-- Attach to whatever facedir is "mounted to".
-- For facedir, this is where tile no. 5 point at.
-- The fallback vector here is in case 'facedir to dir' is nil due
-- to voxelmanip placing a wallmounted node without resetting a
-- pre-existing param2 value that is out-of-range for facedir.
-- The fallback vector corresponds to param2 = 0.
d = core.facedir_to_dir(n.param2) or vector.new(0, 0, 1)
elseif (def.paramtype2 == "4dir" or
def.paramtype2 == "color4dir") then
-- Similar to facedir handling
d = core.fourdir_to_dir(n.param2) or vector.new(0, 0, 1)
end
elseif def.paramtype2 == "wallmounted" or
def.paramtype2 == "colorwallmounted" then
-- Attach to whatever this node is "mounted to".
-- This where tile no. 2 points at.
-- The fallback vector here is used for the same reason as
-- for facedir nodes.
d = core.wallmounted_to_dir(n.param2) or vector.new(0, 1, 0)
else
d.y = -1
end
local p2 = vector.add(p, d)
local nn = core.get_node(p2).name
local def2 = core.registered_nodes[nn]
if def2 and not def2.walkable then
return false
end
return true
end
return function(itemstack, placer, pointed_thing, param2)
-------------------
-- MINETEST CODE --
-------------------
local def = itemstack:get_definition()
if def.type ~= "node" or pointed_thing.type ~= "node" then
return itemstack
end
local under = pointed_thing.under
local oldnode_under = minetest.get_node_or_nil(under)
local above = pointed_thing.above
local oldnode_above = minetest.get_node_or_nil(above)
local playername = user_name(placer)
local log = make_log(playername)
if not oldnode_under or not oldnode_above then
log("info", playername .. " tried to place"
.. " node in unloaded position " .. minetest.pos_to_string(above))
return itemstack
end
local olddef_under = minetest.registered_nodes[oldnode_under.name]
olddef_under = olddef_under or minetest.nodedef_default
local olddef_above = minetest.registered_nodes[oldnode_above.name]
olddef_above = olddef_above or minetest.nodedef_default
if not olddef_above.buildable_to and not olddef_under.buildable_to then
log("info", playername .. " tried to place"
.. " node in invalid position " .. minetest.pos_to_string(above)
.. ", replacing " .. oldnode_above.name)
return itemstack
end
---------------------
-- CUSTOMIZED CODE --
---------------------
-- Place above pointed node
local place_to = vector.copy(above)
-- If node under is buildable_to, check for callback result and place into it instead
if olddef_under.buildable_to and not func(oldnode_under.name) then
log("info", "node under is buildable to")
place_to = vector.copy(under)
end
-------------------
-- MINETEST CODE --
-------------------
if minetest.is_protected(place_to, playername) then
log("action", playername
.. " tried to place " .. def.name
.. " at protected position "
.. minetest.pos_to_string(place_to))
minetest.record_protection_violation(place_to, playername)
return itemstack
end
local oldnode = minetest.get_node(place_to)
local newnode = {name = def.name, param1 = 0, param2 = param2 or 0}
-- Calculate direction for wall mounted stuff like torches and signs
if def.place_param2 ~= nil then
newnode.param2 = def.place_param2
elseif (def.paramtype2 == "wallmounted" or
def.paramtype2 == "colorwallmounted") and not param2 then
local dir = vector.subtract(under, above)
newnode.param2 = minetest.dir_to_wallmounted(dir)
-- Calculate the direction for furnaces and chests and stuff
elseif (def.paramtype2 == "facedir" or
def.paramtype2 == "colorfacedir" or
def.paramtype2 == "4dir" or
def.paramtype2 == "color4dir") and not param2 then
local placer_pos = placer and placer:get_pos()
if placer_pos then
local dir = vector.subtract(above, placer_pos)
newnode.param2 = minetest.dir_to_facedir(dir)
log("info", "facedir: " .. newnode.param2)
end
end
local metatable = itemstack:get_meta():to_table().fields
-- Transfer color information
if metatable.palette_index and not def.place_param2 then
local color_divisor = nil
if def.paramtype2 == "color" then
color_divisor = 1
elseif def.paramtype2 == "colorwallmounted" then
color_divisor = 8
elseif def.paramtype2 == "colorfacedir" then
color_divisor = 32
elseif def.paramtype2 == "color4dir" then
color_divisor = 4
elseif def.paramtype2 == "colordegrotate" then
color_divisor = 32
end
if color_divisor then
local color = math.floor(metatable.palette_index / color_divisor)
local other = newnode.param2 % color_divisor
newnode.param2 = color * color_divisor + other
end
end
-- Check if the node is attached and if it can be placed there
local an = minetest.get_item_group(def.name, "attached_node")
if an ~= 0 and
not check_attached_node(place_to, newnode, an) then
log("action", "attached node " .. def.name ..
" cannot be placed at " .. minetest.pos_to_string(place_to))
return itemstack
end
log("action", playername .. " places node "
.. def.name .. " at " .. minetest.pos_to_string(place_to))
-- Add node and update
minetest.add_node(place_to, newnode)
-- Play sound if it was done by a player
if playername ~= "" and def.sounds and def.sounds.place then
minetest.sound_play(def.sounds.place, {
pos = place_to,
exclude_player = playername,
}, true)
end
local take_item = true
-- Run callback
if def.after_place_node then
-- Deepcopy place_to and pointed_thing because callback can modify it
local place_to_copy = vector.copy(place_to)
local pointed_thing_copy = copy_pointed_thing(pointed_thing)
if def.after_place_node(place_to_copy, placer, itemstack,
pointed_thing_copy) then
take_item = false
end
end
-- Run script hook
for _, callback in ipairs(minetest.registered_on_placenodes) do
-- Deepcopy pos, node and pointed_thing because callback can modify them
local place_to_copy = vector.copy(place_to)
local newnode_copy = {name = newnode.name, param1 = newnode.param1, param2 = newnode.param2}
local oldnode_copy = {name = oldnode.name, param1 = oldnode.param1, param2 = oldnode.param2}
local pointed_thing_copy = copy_pointed_thing(pointed_thing)
if callback(place_to_copy, newnode_copy, placer, oldnode_copy, itemstack, pointed_thing_copy) then
take_item = false
end
end
if take_item then
itemstack:take_item()
end
return itemstack
end
end
--[[Check for a protection violation in a given area. --[[Check for a protection violation in a given area.
-- --
-- Applies is_protected() to a 3D lattice of points in the defined volume. The points are spaced -- Applies is_protected() to a 3D lattice of points in the defined volume. The points are spaced
@ -1032,33 +612,6 @@ function mcl_util.check_position_protection(position, player)
return false return false
end end
local palette_indexes = {grass_palette_index = 0, foliage_palette_index = 0, water_palette_index = 0}
function mcl_util.get_palette_indexes_from_pos(pos)
local biome_data = minetest.get_biome_data(pos)
local biome = biome_data.biome
local biome_name = minetest.get_biome_name(biome)
local reg_biome = minetest.registered_biomes[biome_name]
if reg_biome and reg_biome._mcl_grass_palette_index and reg_biome._mcl_foliage_palette_index and reg_biome._mcl_water_palette_index then
local gpi = reg_biome._mcl_grass_palette_index
local fpi = reg_biome._mcl_foliage_palette_index
local wpi = reg_biome._mcl_water_palette_index
local palette_indexes = {grass_palette_index = gpi, foliage_palette_index = fpi, water_palette_index = wpi}
return palette_indexes
else
return palette_indexes
end
end
function mcl_util.get_colorwallmounted_rotation(pos)
local colorwallmounted_node = minetest.get_node(pos)
for i = 0, 32, 1 do
local colorwallmounted_rotation = colorwallmounted_node.param2 - (i * 8)
if colorwallmounted_rotation < 6 then
return colorwallmounted_rotation
end
end
end
---Move items from one inventory list to another, drop items that do not fit in provided pos and direction. ---Move items from one inventory list to another, drop items that do not fit in provided pos and direction.
---@param src_inv mt.InvRef ---@param src_inv mt.InvRef
---@param src_listname string ---@param src_listname string
@ -1131,3 +684,27 @@ if not vector.in_area then
(pos.z >= min.z) and (pos.z <= max.z) (pos.z >= min.z) and (pos.z <= max.z)
end end
end end
function mcl_util.gen_uuid()
-- Generate a random 128-bit ID that can be assumed to be unique
-- To have a 1% chance of a collision, there would have to be 1.6x10^76 IDs generated
-- https://en.wikipedia.org/wiki/Birthday_problem#Probability_table
local u = {}
for i = 1,16 do
u[#u + 1] = string.format("%02X",math.random(1,255))
end
return table.concat(u)
end
function mcl_util.get_entity_id(entity)
if entity:is_player() then
return entity:get_player_name()
else
local le = entity:get_luaentity()
local id = le._uuid
if not id then
id = mcl_util.gen_uuid()
le._uuid = id
end
return id
end
end

View File

@ -0,0 +1,440 @@
-- Functions related to nodes and node definitions
-- Minetest 5.3.0 or less can only measure the light level. This came in at 5.4
-- This function has been known to fail in multiple places so the error handling is added increase safety and improve
-- debugging. See:
-- https://git.minetest.land/VoxeLibre/VoxeLibre/issues/1392
function mcl_util.get_natural_light (pos, time)
local status, retVal = pcall(minetest.get_natural_light, pos, time)
if status then
return retVal
else
minetest.log("warning", "Failed to get natural light at pos: " .. dump(pos) .. ", time: " .. dump(time))
if (pos) then
local node = minetest.get_node(pos)
minetest.log("warning", "Node at pos: " .. dump(node.name))
end
end
return 0
end
-- Based on minetest.rotate_and_place
--[[
Attempt to predict the desired orientation of the pillar-like node
defined by `itemstack`, and place it accordingly in one of 3 possible
orientations (X, Y or Z).
Stacks are handled normally if the `infinitestacks`
field is false or omitted (else, the itemstack is not changed).
* `invert_wall`: if `true`, place wall-orientation on the ground and ground-
orientation on wall
This function is a simplified version of minetest.rotate_and_place.
The Minetest function is seen as inappropriate because this includes mirror
images of possible orientations, causing problems with pillar shadings.
]]
function mcl_util.rotate_axis_and_place(itemstack, placer, pointed_thing, infinitestacks, invert_wall)
local unode = minetest.get_node_or_nil(pointed_thing.under)
if not unode then
return
end
local undef = minetest.registered_nodes[unode.name]
if undef and undef.on_rightclick and not invert_wall then
undef.on_rightclick(pointed_thing.under, unode, placer,
itemstack, pointed_thing)
return
end
local wield_name = itemstack:get_name()
local above = pointed_thing.above
local under = pointed_thing.under
local anode = minetest.get_node_or_nil(above)
if not anode then
return
end
local pos = pointed_thing.above
local node = anode
if undef and undef.buildable_to then
pos = pointed_thing.under
node = unode
end
if minetest.is_protected(pos, placer:get_player_name()) then
minetest.record_protection_violation(pos, placer:get_player_name())
return
end
local ndef = minetest.registered_nodes[node.name]
if not ndef or not ndef.buildable_to then
return
end
local p2
if above.y ~= under.y then
p2 = 0
elseif above.x ~= under.x then
p2 = 12
elseif above.z ~= under.z then
p2 = 6
end
minetest.set_node(pos, {name = wield_name, param2 = p2})
if not infinitestacks then
itemstack:take_item()
return itemstack
end
end
-- Wrapper of above function for use as `on_place` callback (Recommended).
-- Similar to minetest.rotate_node.
function mcl_util.rotate_axis(itemstack, placer, pointed_thing)
mcl_util.rotate_axis_and_place(itemstack, placer, pointed_thing,
minetest.is_creative_enabled(placer:get_player_name()),
placer:get_player_control().sneak)
return itemstack
end
-- Returns position of the neighbor of a double chest node
-- or nil if node is invalid.
-- This function assumes that the large chest is actually intact
-- * pos: Position of the node to investigate
-- * param2: param2 of that node
-- * side: Which "half" the investigated node is. "left" or "right"
function mcl_util.get_double_container_neighbor_pos(pos, param2, side)
local sign = (side == "right" and 1 or -1)
if param2 == 0 then
return vector.offset(pos, -sign, 0, 0)
elseif param2 == 1 then
return vector.offset(pos, 0, 0, sign)
elseif param2 == 2 then
return vector.offset(pos, sign, 0, 0)
elseif param2 == 3 then
return vector.offset(pos, 0, 0, -sign)
end
end
-- Returns a on_place function for plants
-- * condition: function(pos, node, itemstack)
-- * A function which is called by the on_place function to check if the node can be placed
-- * Must return true, if placement is allowed, false otherwise.
-- * If it returns a string, placement is allowed, but will place this itemstring as a node instead
-- * pos, node: Position and node table of plant node
-- * itemstack: Itemstack to place
function mcl_util.generate_on_place_plant_function(condition)
return function(itemstack, placer, pointed_thing)
if pointed_thing.type ~= "node" then
-- no interaction possible with entities
return itemstack
end
-- Call on_rightclick if the pointed node defines it
local node = minetest.get_node(pointed_thing.under)
local node_def = minetest.registered_nodes[node.name]
if placer and not placer:get_player_control().sneak then
if node_def and node_def.on_rightclick then
return node_def.on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack
end
end
local place_pos
local def_under = minetest.registered_nodes[minetest.get_node(pointed_thing.under).name]
local def_above = minetest.registered_nodes[minetest.get_node(pointed_thing.above).name]
if not def_under or not def_above then
return itemstack
end
if def_under.buildable_to and def_under.name ~= itemstack:get_name() then
place_pos = pointed_thing.under
elseif def_above.buildable_to and def_above.name ~= itemstack:get_name() then
place_pos = pointed_thing.above
pointed_thing.under = pointed_thing.above
else
return itemstack
end
-- Check placement rules
local result, param2 = condition(place_pos, node, itemstack)
if result == true then
local idef = itemstack:get_definition()
local new_itemstack, success = minetest.item_place_node(itemstack, placer, pointed_thing, param2)
if success then
if idef.sounds and idef.sounds.place then
minetest.sound_play(idef.sounds.place, {pos = pointed_thing.above, gain = 1}, true)
end
end
itemstack = new_itemstack
end
return itemstack
end
end
---Return a function to use in `on_place`.
---
---Allow to bypass the `buildable_to` node field in a `on_place` callback.
---
---You have to make sure that the nodes you return true for have `buildable_to = true`.
---@param func fun(node_name: string): boolean Return `true` if node must not replace the buildable_to node which have `node_name`
---@return fun(itemstack: ItemStack, placer: ObjectRef, pointed_thing: pointed_thing, param2: integer): ItemStack?
function mcl_util.bypass_buildable_to(func)
-- Copied from minetest builtin
-- https://github.com/minetest/minetest/blob/526a2f7b8c45504088e194a83d54a19045227bbd/builtin/game/item.lua#L5-L12
local function copy_pointed_thing(pointed_thing)
return {
type = pointed_thing.type,
above = pointed_thing.above and vector.copy(pointed_thing.above),
under = pointed_thing.under and vector.copy(pointed_thing.under),
ref = pointed_thing.ref,
}
end
-- Copied from minetest builtin
-- https://github.com/minetest/minetest/blob/526a2f7b8c45504088e194a83d54a19045227bbd/builtin/game/item.lua#L137-L139
local function user_name(user)
return user and user:get_player_name() or ""
end
-- Returns a logging function. For empty names, does not log. Copied from minetest builtin
-- https://github.com/minetest/minetest/blob/526a2f7b8c45504088e194a83d54a19045227bbd/builtin/game/item.lua#L142-L144
local function make_log(name)
return name ~= "" and minetest.log or function() end
end
-- Copied from minetest builtin
-- https://github.com/minetest/minetest/blob/526a2f7b8c45504088e194a83d54a19045227bbd/builtin/game/falling.lua#L503-L547
local function check_attached_node(p, n, group_rating)
local def = core.registered_nodes[n.name]
local d = vector.zero()
if group_rating == 3 then
-- always attach to floor
d.y = -1
elseif group_rating == 4 then
-- always attach to ceiling
d.y = 1
elseif group_rating == 2 then
-- attach to facedir or 4dir direction
if (def.paramtype2 == "facedir" or
def.paramtype2 == "colorfacedir") then
-- Attach to whatever facedir is "mounted to".
-- For facedir, this is where tile no. 5 point at.
-- The fallback vector here is in case 'facedir to dir' is nil due
-- to voxelmanip placing a wallmounted node without resetting a
-- pre-existing param2 value that is out-of-range for facedir.
-- The fallback vector corresponds to param2 = 0.
d = core.facedir_to_dir(n.param2) or vector.new(0, 0, 1)
elseif (def.paramtype2 == "4dir" or
def.paramtype2 == "color4dir") then
-- Similar to facedir handling
d = core.fourdir_to_dir(n.param2) or vector.new(0, 0, 1)
end
elseif def.paramtype2 == "wallmounted" or
def.paramtype2 == "colorwallmounted" then
-- Attach to whatever this node is "mounted to".
-- This where tile no. 2 points at.
-- The fallback vector here is used for the same reason as
-- for facedir nodes.
d = core.wallmounted_to_dir(n.param2) or vector.new(0, 1, 0)
else
d.y = -1
end
local p2 = vector.add(p, d)
local nn = core.get_node(p2).name
local def2 = core.registered_nodes[nn]
return not def2 or def2.walkable
end
-- Copied from minetest builtin
-- https://github.com/minetest/minetest/blob/e7dd9737bd5deb573c9fef7b3ff2ead29b2cfe31/builtin/game/item.lua#L146-L294
return function(itemstack, placer, pointed_thing, param2)
local def = itemstack:get_definition()
if def.type ~= "node" or pointed_thing.type ~= "node" then
return itemstack
end
local under = pointed_thing.under
local oldnode_under = minetest.get_node_or_nil(under)
local above = pointed_thing.above
local oldnode_above = minetest.get_node_or_nil(above)
local playername = user_name(placer)
local log = make_log(playername)
if not oldnode_under or not oldnode_above then
log("info", playername .. " tried to place"
.. " node in unloaded position " .. minetest.pos_to_string(above))
return itemstack
end
local olddef_under = minetest.registered_nodes[oldnode_under.name] or minetest.nodedef_default
local olddef_above = minetest.registered_nodes[oldnode_above.name] or minetest.nodedef_default
if not olddef_above.buildable_to and not olddef_under.buildable_to then
log("info", playername .. " tried to place"
.. " node in invalid position " .. minetest.pos_to_string(above)
.. ", replacing " .. oldnode_above.name)
return itemstack
end
-- Place above pointed node
local place_to = above
-- If node under is buildable_to, check for callback result and place into it instead
-- This line was modified from minetest code to allow overriding builtable_to
if olddef_under.buildable_to and not func(oldnode_under.name) then
log("info", "node under is buildable to")
place_to = under
end
if minetest.is_protected(place_to, playername) then
log("action", playername
.. " tried to place " .. def.name
.. " at protected position "
.. minetest.pos_to_string(place_to))
minetest.record_protection_violation(place_to, playername)
return itemstack
end
local oldnode = minetest.get_node(place_to)
local newnode = {name = def.name, param1 = 0, param2 = param2 or 0}
-- Calculate direction for wall mounted stuff like torches and signs
if def.place_param2 ~= nil then
newnode.param2 = def.place_param2
elseif (def.paramtype2 == "wallmounted" or
def.paramtype2 == "colorwallmounted") and not param2 then
newnode.param2 = minetest.dir_to_wallmounted(vector.subtract(under, above))
-- Calculate the direction for furnaces and chests and stuff
elseif (def.paramtype2 == "facedir" or
def.paramtype2 == "colorfacedir" or
def.paramtype2 == "4dir" or
def.paramtype2 == "color4dir") and not param2 then
local placer_pos = placer and placer:get_pos()
if placer_pos then
newnode.param2 = minetest.dir_to_facedir(vector.subtract(above, placer_pos))
log("info", "facedir: " .. newnode.param2)
end
end
local metatable = itemstack:get_meta():to_table().fields
-- Transfer color information
if metatable.palette_index and not def.place_param2 then
local color_divisor = nil
if def.paramtype2 == "color" then
color_divisor = 1
elseif def.paramtype2 == "colorwallmounted" then
color_divisor = 8
elseif def.paramtype2 == "colorfacedir" then
color_divisor = 32
elseif def.paramtype2 == "color4dir" then
color_divisor = 4
elseif def.paramtype2 == "colordegrotate" then
color_divisor = 32
end
if color_divisor then
local color = math.floor(metatable.palette_index / color_divisor)
local other = newnode.param2 % color_divisor
newnode.param2 = color * color_divisor + other
end
end
-- Check if the node is attached and if it can be placed there
local an = minetest.get_item_group(def.name, "attached_node")
if an ~= 0 and
not check_attached_node(place_to, newnode, an) then
log("action", "attached node " .. def.name ..
" cannot be placed at " .. minetest.pos_to_string(place_to))
return itemstack
end
log("action", playername .. " places node "
.. def.name .. " at " .. minetest.pos_to_string(place_to))
-- Add node and update
minetest.add_node(place_to, newnode)
-- Play sound if it was done by a player
if playername ~= "" and def.sounds and def.sounds.place then
minetest.sound_play(def.sounds.place, {
pos = place_to,
exclude_player = playername,
}, true)
end
local take_item = true
-- Run callback
if def.after_place_node then
-- Deepcopy place_to and pointed_thing because callback can modify it
local place_to_copy = vector.copy(place_to)
local pointed_thing_copy = copy_pointed_thing(pointed_thing)
if def.after_place_node(place_to_copy, placer, itemstack,
pointed_thing_copy) then
take_item = false
end
end
-- Run script hook
for _, callback in ipairs(minetest.registered_on_placenodes) do
-- Deepcopy pos, node and pointed_thing because callback can modify them
local place_to_copy = vector.copy(place_to)
local newnode_copy = {name = newnode.name, param1 = newnode.param1, param2 = newnode.param2}
local oldnode_copy = {name = oldnode.name, param1 = oldnode.param1, param2 = oldnode.param2}
local pointed_thing_copy = copy_pointed_thing(pointed_thing)
if callback(place_to_copy, newnode_copy, placer, oldnode_copy, itemstack, pointed_thing_copy) then
take_item = false
end
end
if take_item then
itemstack:take_item()
end
return itemstack
end
end
local DEFAULT_PALETTE_INDEXES = {grass_palette_index = 0, foliage_palette_index = 0, water_palette_index = 0}
function mcl_util.get_palette_indexes_from_pos(pos)
local biome_data = minetest.get_biome_data(pos)
local biome = biome_data.biome
local biome_name = minetest.get_biome_name(biome)
local reg_biome = minetest.registered_biomes[biome_name]
if reg_biome and reg_biome._mcl_grass_palette_index and reg_biome._mcl_foliage_palette_index and reg_biome._mcl_water_palette_index then
return {
grass_palette_index = reg_biome._mcl_grass_palette_index,
foliage_palette_index = reg_biome._mcl_foliage_palette_index,
water_palette_index = reg_biome._mcl_water_palette_index,
}
else
return DEFAULT_PALETTE_INDEXES
end
end
function mcl_util.get_colorwallmounted_rotation(pos)
local colorwallmounted_node = minetest.get_node(pos)
for i = 0, 32, 1 do
local colorwallmounted_rotation = colorwallmounted_node.param2 - (i * 8)
if colorwallmounted_rotation < 6 then
return colorwallmounted_rotation
end
end
end
function mcl_util.match_node_to_filter(node_name, filters)
for i = 1,#filters do
local filter = filters[i]
if node_name == filter then return true end
if string.sub(filter,1,6) == "group:" and minetest.get_item_group(node_name, string.sub(filter,7)) ~= 0 then return true end
end
return false
end

View File

@ -1236,13 +1236,14 @@ function mob_class:do_states_attack (dtime)
minetest.after(1, function() minetest.after(1, function()
self.firing = false self.firing = false
end) end)
arrow = minetest.add_entity(p, self.arrow)
arrow = vl_projectile.create(self.arrow, {
pos = p,
owner = self,
})
ent = arrow:get_luaentity() ent = arrow:get_luaentity()
if ent.velocity then v = ent.velocity or v
v = ent.velocity
end
ent.switch = 1 ent.switch = 1
ent.owner_id = tostring(self.object) -- add unique owner id to arrow
-- important for mcl_shields -- important for mcl_shields
ent._shooter = self.object ent._shooter = self.object
@ -1252,12 +1253,11 @@ function mob_class:do_states_attack (dtime)
end end
end end
local amount = (vec.x * vec.x + vec.y * vec.y + vec.z * vec.z) ^ 0.5
-- offset makes shoot aim accurate -- offset makes shoot aim accurate
local amount = vector.length(vec)
vec.y = vec.y + self.shoot_offset vec.y = vec.y + self.shoot_offset
vec.x = vec.x * (v / amount) vec = vector.multiply(vec, v / amount)
vec.y = vec.y * (v / amount)
vec.z = vec.z * (v / amount)
if self.shoot_arrow then if self.shoot_arrow then
vec = vector.normalize(vec) vec = vector.normalize(vec)
self:shoot_arrow(p, vec) self:shoot_arrow(p, vec)

View File

@ -383,11 +383,17 @@ end
-- register arrow for shoot attack -- register arrow for shoot attack
function mcl_mobs.register_arrow(name, def) function mcl_mobs.register_arrow(name, def)
if not name or not def then return end -- errorcheck if not name or not def then return end -- errorcheck
minetest.register_entity(name, { local behaviors = {}
if def.hit_node then
table.insert(behaviors, vl_projectile.collides_with_solids)
end
if def.hit_player or def.hit_mob or def.hit_object then
table.insert(behaviors, vl_projectile.collides_with_entities)
end
vl_projectile.register(name, {
physical = false, physical = false,
visual = def.visual, visual = def.visual,
visual_size = def.visual_size, visual_size = def.visual_size,
@ -399,43 +405,87 @@ function mcl_mobs.register_arrow(name, def)
hit_object = def.hit_object, hit_object = def.hit_object,
homing = def.homing, homing = def.homing,
drop = def.drop or false, -- drops arrow as registered item when true drop = def.drop or false, -- drops arrow as registered item when true
collisionbox = {0, 0, 0, 0, 0, 0}, -- remove box around arrows collisionbox = def.collisionbox or {0, 0, 0, 0, 0, 0}, -- remove box around arrows
timer = 0, timer = 0,
switch = 0, switch = 0,
_lifetime = def._lifetime or 7, _lifetime = def._lifetime or 7,
owner_id = def.owner_id, owner_id = def.owner_id,
rotate = def.rotate, _vl_projectile = {
behaviors = behaviors,
ignore_gravity = true,
damages_players = true,
allow_punching = function(self, entity_def, projectile_def, object)
if def.allow_punching and not def.allow_punching(self, entity_def, projectile_def, object) then return false end
if self.timer > 2 then return true end
if self._owner and mcl_util.get_entity_id(object) == self._owner then return false end
return true
end,
on_collide_with_solid = function(self, pos, node, nodedef)
if nodedef or not nodedef.walkable then return end
self.hit_node(self, pos, node)
if self.drop == true then
pos.y = pos.y + 1
self.lastpos = self.lastpos or pos
minetest.add_item(self.lastpos, self.object:get_luaentity().name)
end
self._removed = true
self.object:remove();
end,
on_collide_with_entity = function(self, pos, object)
if self.hit_player and object:is_player() then
self.hit_player(self, object)
self._removed = true
self.object:remove()
return
end
local entity = object:get_luaentity()
if not entity or entity.name == self.object:get_luaentity().name then return end
if self.timer <= 2 then return end
if self.hit_mob and entity.is_mob == true then
self.hit_mob(self, object)
self._removed = true
self.object:remove()
return
elseif self.hit_object then
self.hit_object(self, object)
self._removed = true
self.object:remove()
return
end
end
},
on_punch = def.on_punch or function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage) on_punch = def.on_punch or function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
local vel = self.object:get_velocity():length() local vel = self.object:get_velocity():length()
self.object:set_velocity(dir * vel) self.object:set_velocity(dir * vel)
self._puncher = puncher self._owner = mcl_util.get_entity_id(puncher)
end, end,
collisionbox = def.collisionbox or {0, 0, 0, 0, 0, 0},
automatic_face_movement_dir = def.rotate automatic_face_movement_dir = def.rotate
and (def.rotate - (math.pi / 180)) or false, and (def.rotate - (math.pi / 180)) or false,
on_activate = def.on_activate, on_activate = def.on_activate,
on_step = def.on_step or function(self, dtime) on_step = def.on_step or function(self, dtime)
-- Projectile behavior processing
self.timer = self.timer + dtime vl_projectile.update_projectile(self, dtime)
local pos = self.object:get_pos() local pos = self.object:get_pos()
if not pos then return end
if self.switch == 0 if self.switch == 0 or self.timer > self._lifetime or not within_limits(pos, 0) then
or self.timer > self._lifetime
or not within_limits(pos, 0) then
mcl_burning.extinguish(self.object) mcl_burning.extinguish(self.object)
self._removed = true
self.object:remove(); self.object:remove();
return return
end end
-- does arrow have a tail (fireball) -- does arrow have a tail (fireball)
if def.tail if def.tail == 1 and def.tail_texture then
and def.tail == 1
and def.tail_texture then
minetest.add_particle({ minetest.add_particle({
pos = pos, pos = pos,
velocity = {x = 0, y = 0, z = 0}, velocity = {x = 0, y = 0, z = 0},
@ -448,29 +498,6 @@ function mcl_mobs.register_arrow(name, def)
}) })
end end
if self.hit_node then
local node = node_ok(pos).name
if minetest.registered_nodes[node].walkable then
self.hit_node(self, pos, node)
if self.drop == true then
pos.y = pos.y + 1
self.lastpos = (self.lastpos or pos)
minetest.add_item(self.lastpos, self.object:get_luaentity().name)
end
self.object:remove();
return
end
end
if self.homing and self._target then if self.homing and self._target then
local p = self._target:get_pos() local p = self._target:get_pos()
if p then if p then
@ -482,42 +509,6 @@ function mcl_mobs.register_arrow(name, def)
end end
end end
if self.hit_player or self.hit_mob or self.hit_object then
for _,object in pairs(minetest.get_objects_inside_radius(pos, 1.5)) do
if self.hit_player
and object:is_player() then
self.hit_player(self, object)
self.object:remove();
return
end
local entity = object:get_luaentity()
if entity
and self.hit_mob
and entity.is_mob == true
and (tostring(object) ~= self.owner_id or self.timer > 2)
and entity.name ~= self.object:get_luaentity().name then
self.hit_mob(self, object)
self.object:remove();
return
end
if entity
and self.hit_object
and (not entity.is_mob)
and (tostring(object) ~= self.owner_id or self.timer > 2)
and entity.name ~= self.object:get_luaentity().name then
self.hit_object(self, object)
self.object:remove();
return
end
end
end
self.lastpos = pos self.lastpos = pos
end end
}) })

View File

@ -1,5 +1,5 @@
name = mcl_mobs name = mcl_mobs
author = PilzAdam author = PilzAdam
description = Adds a mob API for mods to add animals or monsters, etc. description = Adds a mob API for mods to add animals or monsters, etc.
depends = mcl_particles, mcl_luck depends = mcl_particles, mcl_luck, vl_projectile
optional_depends = mcl_weather, mcl_explosions, mcl_hunger, mcl_worlds, invisibility, lucky_block, cmi, doc_identifier, mcl_armor, mcl_portals, mcl_experience, mcl_sculk optional_depends = mcl_weather, mcl_explosions, mcl_hunger, mcl_worlds, invisibility, lucky_block, cmi, doc_identifier, mcl_armor, mcl_portals, mcl_experience, mcl_sculk

View File

@ -133,8 +133,8 @@ mcl_mobs.register_arrow("mobs_mc:fireball", {
}, nil) }, nil)
mcl_mobs.mob_class.boom(self,self.object:get_pos(), 1, true) mcl_mobs.mob_class.boom(self,self.object:get_pos(), 1, true)
local ent = mob:get_luaentity() local ent = mob:get_luaentity()
if (not ent or ent.health <= 0) and self._puncher and name == "mobs_mc:ghast" then if (not ent or ent.health <= 0) and self._owner and minetest.get_player_by_name(self._owner) and name == "mobs_mc:ghast" then
awards.unlock(self._puncher:get_player_name(), "mcl:fireball_redir_serv") awards.unlock(self._owner, "mcl:fireball_redir_serv")
end end
end, end,

View File

@ -201,6 +201,7 @@ mcl_mobs.register_mob("mobs_mc:wither", {
self._death_timer = self._death_timer + self.health - self._health_old self._death_timer = self._death_timer + self.health - self._health_old
if self.health == self._health_old then self._death_timer = self._death_timer + dtime end if self.health == self._health_old then self._death_timer = self._death_timer + dtime end
if self._death_timer > 100 then if self._death_timer > 100 then
self._removed = true
self.object:remove() self.object:remove()
return false return false
end end
@ -456,15 +457,18 @@ mcl_mobs.register_arrow("mobs_mc:wither_skull", {
textures = { textures = {
"mobs_mc_wither_projectile.png^[verticalframe:6:0", -- top "mobs_mc_wither_projectile.png^[verticalframe:6:0", -- top
"mobs_mc_wither_projectile.png^[verticalframe:6:1", -- bottom "mobs_mc_wither_projectile.png^[verticalframe:6:1", -- bottom
"mobs_mc_wither_projectile.png^[verticalframe:6:2", -- left
"mobs_mc_wither_projectile.png^[verticalframe:6:3", -- right
"mobs_mc_wither_projectile.png^[verticalframe:6:4", -- back "mobs_mc_wither_projectile.png^[verticalframe:6:4", -- back
"mobs_mc_wither_projectile.png^[verticalframe:6:5", -- front "mobs_mc_wither_projectile.png^[verticalframe:6:5", -- front
"mobs_mc_wither_projectile.png^[verticalframe:6:3", -- right
"mobs_mc_wither_projectile.png^[verticalframe:6:2", -- left
}, },
velocity = 7, velocity = 7,
rotate = 90,
_lifetime = 15, _lifetime = 15,
on_punch = function(self) end, on_punch = function(self) end,
allow_punching = function(self, _, _, object)
local le = object and object:get_luaentity()
return not le or le.name ~= "mobs_mc:wither"
end,
-- direct hit -- direct hit
hit_player = function(self, player) hit_player = function(self, player)
@ -509,15 +513,18 @@ mcl_mobs.register_arrow("mobs_mc:wither_skull_strong", {
textures = { textures = {
"mobs_mc_wither_projectile_strong.png^[verticalframe:6:0", -- top "mobs_mc_wither_projectile_strong.png^[verticalframe:6:0", -- top
"mobs_mc_wither_projectile_strong.png^[verticalframe:6:1", -- bottom "mobs_mc_wither_projectile_strong.png^[verticalframe:6:1", -- bottom
"mobs_mc_wither_projectile_strong.png^[verticalframe:6:2", -- left
"mobs_mc_wither_projectile_strong.png^[verticalframe:6:3", -- right
"mobs_mc_wither_projectile_strong.png^[verticalframe:6:4", -- back "mobs_mc_wither_projectile_strong.png^[verticalframe:6:4", -- back
"mobs_mc_wither_projectile_strong.png^[verticalframe:6:5", -- front "mobs_mc_wither_projectile_strong.png^[verticalframe:6:5", -- front
"mobs_mc_wither_projectile_strong.png^[verticalframe:6:3", -- right
"mobs_mc_wither_projectile_strong.png^[verticalframe:6:2", -- left
}, },
velocity = 4, velocity = 4,
rotate = 90,
_lifetime = 25, _lifetime = 25,
on_punch = function(self) end, on_punch = function(self) end,
allow_punching = function(self, _, _, object)
local le = object and object:get_luaentity()
return not le or le.name ~= "mobs_mc:wither"
end,
-- direct hit -- direct hit
hit_player = function(self, player) hit_player = function(self, player)

View File

@ -27,6 +27,11 @@ minetest.register_node("mcl_target:target_off", {
rules = mesecon.rules.alldirs, rules = mesecon.rules.alldirs,
}, },
}, },
_vl_projectile = {
on_collide = function(projectile, pos, node, node_def)
mcl_target.hit(pos, 1) --10 redstone ticks
end
},
_mcl_blast_resistance = 0.5, _mcl_blast_resistance = 0.5,
_mcl_hardness = 0.5, _mcl_hardness = 0.5,
}) })

View File

@ -151,6 +151,16 @@ function mesecon.register_button(basename, description, texture, recipeitem, sou
}}, }},
_mcl_button_basename = basename, _mcl_button_basename = basename,
_mcl_button_timer = button_timer, _mcl_button_timer = button_timer,
_vl_projectile = {
on_collide = function(projectile, pos, node, node_def)
pos = vector.round(pos)
-- Push the button! Push, push, push the button!
if node_def.groups.button_push_by_arrow == 1 then
mesecon.push_button(pos, node)
end
end
},
_mcl_blast_resistance = 0.5, _mcl_blast_resistance = 0.5,
_mcl_hardness = 0.5, _mcl_hardness = 0.5,

View File

@ -7,35 +7,8 @@ local enable_pvp = minetest.settings:get_bool("enable_pvp")
local math = math local math = math
local vector = vector local vector = vector
-- Time in seconds after which a stuck arrow is deleted
local ARROW_TIMEOUT = 60
-- Time after which stuck arrow is rechecked for being stuck
local STUCK_RECHECK_TIME = 5
--local GRAVITY = 9.81
local YAW_OFFSET = -math.pi/2 local YAW_OFFSET = -math.pi/2
local function dir_to_pitch(dir)
--local dir2 = vector.normalize(dir)
local xz = math.abs(dir.x) + math.abs(dir.z)
return -math.atan2(-dir.y, xz)
end
local function random_arrow_positions(positions, placement)
if positions == "x" then
return math.random(-4, 4)
elseif positions == "y" then
return math.random(0, 10)
end
if placement == "front" and positions == "z" then
return 3
elseif placement == "back" and positions == "z" then
return -3
end
return 0
end
local mod_awards = minetest.get_modpath("awards") and minetest.get_modpath("mcl_achievements") local mod_awards = minetest.get_modpath("awards") and minetest.get_modpath("mcl_achievements")
local mod_button = minetest.get_modpath("mesecons_button") local mod_button = minetest.get_modpath("mesecons_button")
@ -56,7 +29,8 @@ S("Arrows might get stuck on solid blocks and can be retrieved again. They are a
end, end,
}) })
local ARROW_ENTITY={ -- Destroy arrow entity self at pos and drops it as an item
local arrow_entity = {
physical = true, physical = true,
pointable = false, pointable = false,
visual = "mesh", visual = "mesh",
@ -65,9 +39,13 @@ local ARROW_ENTITY={
textures = {"mcl_bows_arrow.png"}, textures = {"mcl_bows_arrow.png"},
collisionbox = {-0.19, -0.125, -0.19, 0.19, 0.125, 0.19}, collisionbox = {-0.19, -0.125, -0.19, 0.19, 0.125, 0.19},
collide_with_objects = false, collide_with_objects = false,
liquid_drag = true,
_fire_damage_resistant = true, _fire_damage_resistant = true,
_lastpos={}, _save_fields = {
"last_pos", "startpos", "damage", "is_critical", "stuck", "stuckin", "stuckin_player", "time_in_air", "vl_projectile",
},
_startpos=nil, _startpos=nil,
_damage=1, -- Damage on impact _damage=1, -- Damage on impact
_is_critical=false, -- Whether this arrow would deal critical damage _is_critical=false, -- Whether this arrow would deal critical damage
@ -81,430 +59,130 @@ local ARROW_ENTITY={
_blocked = false, _blocked = false,
_viscosity=0, -- Viscosity of node the arrow is currently in _viscosity=0, -- Viscosity of node the arrow is currently in
_deflection_cooloff=0, -- Cooloff timer after an arrow deflection, to prevent many deflections in quick succession _deflection_cooloff=0, -- Cooloff timer after an arrow deflection, to prevent many deflections in quick succession
}
-- Destroy arrow entity self at pos and drops it as an item _vl_projectile = {
local function spawn_item(self, pos) survive_collision = true,
if not minetest.is_creative_enabled("") then sticks_in_players = true,
local item = minetest.add_item(pos, "mcl_bows:arrow") damages_players = true,
item:set_velocity(vector.new(0, 0, 0)) maximum_time = 60,
item:set_yaw(self.object:get_yaw()) damage_groups = function(self)
end return { fleshy = self._damage }
mcl_burning.extinguish(self.object) end,
self.object:remove() hide_tracer = function(self)
end return self._stuck or self._damage < 9 or self._in_player
end,
local function damage_particles(pos, is_critical) tracer_texture = "mobs_mc_arrow_particle.png",
if is_critical then behaviors = {
minetest.add_particlespawner({ vl_projectile.sticks,
amount = 15, vl_projectile.burns,
time = 0.1, vl_projectile.has_tracer,
minpos = vector.offset(pos, -0.5, -0.5, -0.5),
maxpos = vector.offset(pos, 0.5, 0.5, 0.5),
minvel = vector.new(-0.1, -0.1, -0.1),
maxvel = vector.new(0.1, 0.1, 0.1),
minexptime = 1,
maxexptime = 2,
minsize = 1.5,
maxsize = 1.5,
collisiondetection = false,
vertical = false,
texture = "mcl_particles_crit.png^[colorize:#bc7a57:127",
})
end
end
function ARROW_ENTITY.on_step(self, dtime)
mcl_burning.tick(self.object, dtime, self)
-- mcl_burning.tick may remove object immediately
if not self.object:get_pos() then return end
self._time_in_air = self._time_in_air + .001
-- Custom arrow behaviors
function(self, dtime)
local pos = self.object:get_pos() local pos = self.object:get_pos()
local dpos = vector.round(vector.new(pos)) -- digital pos self._allow_punch = self._allow_punch or not self._owner or not self._startpos or pos and vector.distance(self._startpos, pos) > 1.5
local node = minetest.get_node(dpos)
if self._stuck then
self._stucktimer = self._stucktimer + dtime
self._stuckrechecktimer = self._stuckrechecktimer + dtime
if self._stucktimer > ARROW_TIMEOUT then
mcl_burning.extinguish(self.object)
self.object:remove()
return
end
-- Drop arrow as item when it is no longer stuck
-- FIXME: Arrows are a bit slow to react and continue to float in mid air for a few seconds.
if self._stuckrechecktimer > STUCK_RECHECK_TIME then
local stuckin_def
if self._stuckin then
stuckin_def = minetest.registered_nodes[minetest.get_node(self._stuckin).name]
end
-- TODO: In MC, arrow just falls down without turning into an item
if stuckin_def and stuckin_def.walkable == false then
spawn_item(self, pos)
return
end
self._stuckrechecktimer = 0
end
-- Pickup arrow if player is nearby (not in Creative Mode)
local objects = minetest.get_objects_inside_radius(pos, 1)
for _,obj in ipairs(objects) do
if obj:is_player() then
if self._collectable and not minetest.is_creative_enabled(obj:get_player_name()) then
if obj:get_inventory():room_for_item("main", "mcl_bows:arrow") then
obj:get_inventory():add_item("main", "mcl_bows:arrow")
minetest.sound_play("item_drop_pickup", {
pos = pos,
max_hear_distance = 16,
gain = 1.0,
}, true)
end
end
mcl_burning.extinguish(self.object)
self.object:remove()
return
end
end
-- Check for object "collision". Done every tick (hopefully this is not too stressing)
else
if self._damage >= 9 and self._in_player == false then
minetest.add_particlespawner({
amount = 20,
time = .2,
minpos = vector.new(0,0,0),
maxpos = vector.new(0,0,0),
minvel = vector.new(-0.1,-0.1,-0.1),
maxvel = vector.new(0.1,0.1,0.1),
minexptime = 0.5,
maxexptime = 0.5,
minsize = 2,
maxsize = 2,
attached = self.object,
collisiondetection = false,
vertical = false,
texture = "mobs_mc_arrow_particle.png",
glow = 1,
})
end
local closest_object
local closest_distance
if self._deflection_cooloff > 0 then if self._deflection_cooloff > 0 then
self._deflection_cooloff = self._deflection_cooloff - dtime self._deflection_cooloff = self._deflection_cooloff - dtime
end end
end,
local arrow_dir = self.object:get_velocity() vl_projectile.collides_with_solids,
--create a raycast from the arrow based on the velocity of the arrow to deal with lag vl_projectile.raycast_collides_with_entities,
local raycast = minetest.raycast(pos, vector.add(pos, vector.multiply(arrow_dir, 0.1)), true, false) },
for hitpoint in raycast do allow_punching = function(self, entity_def, projectile_def, object)
if hitpoint.type == "object" then if not self._allow_punch then return false end
-- find the closest object that is in the way of the arrow
local ok = false local lua = object:get_luaentity()
if hitpoint.ref:is_player() and enable_pvp then if lua and lua.name == "mobs_mc:rover" then return false end
ok = true
elseif not hitpoint.ref:is_player() and hitpoint.ref:get_luaentity() then return true
if (hitpoint.ref:get_luaentity().is_mob or hitpoint.ref:get_luaentity()._hittable_by_projectile) then end,
ok = true sounds = {
end on_entity_collision = function(self, _, _, _, obj)
end if obj:is_player() then
if ok then return {{name="mcl_bows_hit_player", gain=0.1}, {to_player=obj:get_player_name()}, true}
local dist = vector.distance(hitpoint.ref:get_pos(), pos)
if not closest_object or not closest_distance then
closest_object = hitpoint.ref
closest_distance = dist
elseif dist < closest_distance then
closest_object = hitpoint.ref
closest_distance = dist
end
end
end
end end
if closest_object then return {{name="mcl_bows_hit_other", gain=0.3}, {pos=self.object:get_pos(), max_hear_distance=16}, true}
local obj = closest_object end
},
on_collide_with_entity = function(self, pos, obj)
local is_player = obj:is_player() local is_player = obj:is_player()
local lua = obj:get_luaentity() local lua = obj:get_luaentity()
if obj == self._shooter and self._time_in_air > 1.02 or obj ~= self._shooter and (is_player or (lua and (lua.is_mob or lua._hittable_by_projectile))) then
if obj:get_hp() > 0 then -- Make sure collision is valid
-- Check if there is no solid node between arrow and object if not (is_player or (lua and (lua.is_mob or lua._hittable_by_projectile))) then
local ray = minetest.raycast(self.object:get_pos(), obj:get_pos(), true)
for pointed_thing in ray do
if pointed_thing.type == "object" and pointed_thing.ref == closest_object then
-- Target reached! We can proceed now.
break
elseif pointed_thing.type == "node" then
local nn = minetest.get_node(minetest.get_pointed_thing_position(pointed_thing)).name
local def = minetest.registered_nodes[nn]
if (not def) or def.walkable then
-- There's a node in the way. Delete arrow without damage
mcl_burning.extinguish(self.object)
self.object:remove()
return return
end end
end
end
-- Punch target object but avoid hurting enderman.
if not lua or lua.name ~= "mobs_mc:rover" then
if not self._in_player then
damage_particles(vector.add(pos, vector.multiply(self.object:get_velocity(), 0.1)), self._is_critical)
end
if mcl_burning.is_burning(self.object) then
mcl_burning.set_on_fire(obj, 5)
end
if not self._in_player and not self._blocked then
obj:punch(self.object, 1.0, {
full_punch_interval=1.0,
damage_groups={fleshy=self._damage},
}, self.object:get_velocity())
if obj:is_player() then
if not mcl_shields.is_blocking(obj) then
local placement
self._placement = math.random(1, 2)
if self._placement == 1 then
placement = "front"
else
placement = "back"
end
self._in_player = true
if self._placement == 2 then
self._rotation_station = 90
else
self._rotation_station = -90
end
self._y_position = random_arrow_positions("y", placement)
self._x_position = random_arrow_positions("x", placement)
if self._y_position > 6 and self._x_position < 2 and self._x_position > -2 then
self._attach_parent = "Head"
self._y_position = self._y_position - 6
elseif self._x_position > 2 then
self._attach_parent = "Arm_Right"
self._y_position = self._y_position - 3
self._x_position = self._x_position - 2
elseif self._x_position < -2 then
self._attach_parent = "Arm_Left"
self._y_position = self._y_position - 3
self._x_position = self._x_position + 2
else
self._attach_parent = "Body"
end
self._z_rotation = math.random(-30, 30)
self._y_rotation = math.random( -30, 30)
self.object:set_attach(
obj, self._attach_parent,
vector.new(self._x_position, self._y_position, random_arrow_positions("z", placement)),
vector.new(0, self._rotation_station + self._y_rotation, self._z_rotation)
)
else
self._blocked = true
self.object:set_velocity(vector.multiply(self.object:get_velocity(), -0.25))
end
minetest.after(150, function()
self.object:remove()
end)
else
self.object:remove()
end
end
end
if is_player then
if self._shooter and self._shooter:is_player() and not self._in_player and not self._blocked then
-- “Ding” sound for hitting another player
minetest.sound_play({name="mcl_bows_hit_player", gain=0.1}, {to_player=self._shooter:get_player_name()}, true)
end
end
if obj:get_hp() > 0 then
if lua then if lua then
local entity_name = lua.name local entity_name = lua.name
-- Achievement for hitting skeleton, wither skeleton or stray (TODO) with an arrow at least 50 meters away -- Achievement for hitting skeleton, wither skeleton or stray (TODO) with an arrow at least 50 meters away
-- NOTE: Range has been reduced because mobs unload much earlier than that ... >_> -- NOTE: Range has been reduced because mobs unload much earlier than that ... >_>
-- TODO: This achievement should be given for the kill, not just a hit -- TODO: This achievement should be given for the kill, not just a hit
if self._shooter and self._shooter:is_player() and vector.distance(pos, self._startpos) >= 20 then local shooter = self._vl_projectile.owner
if shooter and shooter:is_player() and vector.distance(pos, self._startpos) >= 20 then
if mod_awards and (entity_name == "mobs_mc:skeleton" or entity_name == "mobs_mc:stray" or entity_name == "mobs_mc:witherskeleton") then if mod_awards and (entity_name == "mobs_mc:skeleton" or entity_name == "mobs_mc:stray" or entity_name == "mobs_mc:witherskeleton") then
awards.unlock(self._shooter:get_player_name(), "mcl:snipeSkeleton") awards.unlock(shooter:get_player_name(), "mcl:snipeSkeleton")
end end
end end
end end
if not self._in_player and not self._blocked then
minetest.sound_play({name="mcl_bows_hit_other", gain=0.3}, {pos=self.object:get_pos(), max_hear_distance=16}, true)
end
end
if not obj:is_player() then
mcl_burning.extinguish(self.object)
if self._piercing == 0 then
self.object:remove()
end
end end
-- Item definition entity collision hook
local item_def = minetest.registered_items[self._arrow_item]
local hook = item_def and item_def._on_collide_with_entity
if hook then hook(self, pos, obj) end
if (self._piercing or 0) > 0 then
self._piercing = self._piercing - 1
return return
end end
end
end
-- Check for node collision -- Because arrows are flagged to survive collisions to allow sticking into blocks, manually remove it now that it
if self._lastpos.x~=nil and not self._stuck then -- has collided with an entity
local def = minetest.registered_nodes[node.name] self._removed = true
local vel = self.object:get_velocity() self.object:remove()
-- Arrow has stopped in one axis, so it probably hit something.
-- This detection is a bit clunky, but sadly, MT does not offer a direct collision detection for us. :-(
if (math.abs(vel.x) < 0.0001) or (math.abs(vel.z) < 0.0001) or (math.abs(vel.y) < 0.00001) then
-- Check for the node to which the arrow is pointing
local dir
if math.abs(vel.y) < 0.00001 then
if self._lastpos.y < pos.y then
dir = vector.new(0, 1, 0)
else
dir = vector.new(0, -1, 0)
end end
else },
dir = minetest.facedir_to_dir(minetest.dir_to_facedir(minetest.yaw_to_dir(self.object:get_yaw()-YAW_OFFSET)))
end
self._stuckin = vector.add(dpos, dir)
local snode = minetest.get_node(self._stuckin)
local sdef = minetest.registered_nodes[snode.name]
-- If node is non-walkable, unknown or ignore, don't make arrow stuck. -- Force recheck of stuck arrows when punched.
-- This causes a deflection in the engine. -- Otherwise, punching has no effect.
if not sdef or sdef.walkable == false or snode.name == "ignore" then on_punch = function(self)
self._stuckin = nil
if self._deflection_cooloff <= 0 then
-- Lose 1/3 of velocity on deflection
local newvel = vector.multiply(vel, 0.6667)
self.object:set_velocity(newvel)
-- Reset deflection cooloff timer to prevent many deflections happening in quick succession
self._deflection_cooloff = 1.0
end
else
-- Node was walkable, make arrow stuck
self._stuck = true
self._stucktimer = 0
self._stuckrechecktimer = 0
self.object:set_velocity(vector.new(0, 0, 0))
self.object:set_acceleration(vector.new(0, 0, 0))
minetest.sound_play({name="mcl_bows_hit_other", gain=0.3}, {pos=self.object:get_pos(), max_hear_distance=16}, true)
if mcl_burning.is_burning(self.object) and snode.name == "mcl_tnt:tnt" then
tnt.ignite(self._stuckin)
end
-- Ignite Campfires
if mod_campfire and mcl_burning.is_burning(self.object) and minetest.get_item_group(snode.name, "campfire") ~= 0 then
mcl_campfires.light_campfire(self._stuckin)
end
-- Activate target
if mod_target and snode.name == "mcl_target:target_off" then
mcl_target.hit(self._stuckin, 1) --10 redstone ticks
end
-- Push the button! Push, push, push the button!
if mod_button and minetest.get_item_group(node.name, "button") > 0 and minetest.get_item_group(node.name, "button_push_by_arrow") == 1 then
local bdir = minetest.wallmounted_to_dir(node.param2)
-- Check the button orientation
if vector.equals(vector.add(dpos, bdir), self._stuckin) then
mesecon.push_button(dpos, node)
end
end
end
elseif (def and def.liquidtype ~= "none") then
-- Slow down arrow in liquids
local v = def.liquid_viscosity
if not v then
v = 0
end
--local old_v = self._viscosity
self._viscosity = v
local vpenalty = math.max(0.1, 0.98 - 0.1 * v)
if math.abs(vel.x) > 0.001 then
vel.x = vel.x * vpenalty
end
if math.abs(vel.z) > 0.001 then
vel.z = vel.z * vpenalty
end
self.object:set_velocity(vel)
end
end
-- Update yaw
if not self._stuck then
local vel = self.object:get_velocity()
local yaw = minetest.dir_to_yaw(vel)+YAW_OFFSET
local pitch = dir_to_pitch(vel)
self.object:set_rotation({ x = 0, y = yaw, z = pitch })
end
-- Update internal variable
self._lastpos = pos
end
-- Force recheck of stuck arrows when punched.
-- Otherwise, punching has no effect.
function ARROW_ENTITY.on_punch(self)
if self._stuck then if self._stuck then
self._stuckrechecktimer = STUCK_RECHECK_TIME self._stuckrechecktimer = 5
end
end,
get_staticdata = function(self)
local out = {}
local save_fields = self._save_fields
for i = 1,#save_fields do
local field = save_fields[i]
out[field] = self["_"..field]
end end
end
function ARROW_ENTITY.get_staticdata(self)
local out = {
lastpos = self._lastpos,
startpos = self._startpos,
damage = self._damage,
is_critical = self._is_critical,
stuck = self._stuck,
stuckin = self._stuckin,
stuckin_player = self._in_player,
}
if self._stuck then
-- If _stucktimer is missing for some reason, assume the maximum
if not self._stucktimer then
self._stucktimer = ARROW_TIMEOUT
end
out.stuckstarttime = minetest.get_gametime() - self._stucktimer
end
if self._shooter and self._shooter:is_player() then
out.shootername = self._shooter:get_player_name()
end
return minetest.serialize(out) return minetest.serialize(out)
end end,
on_activate = function(self, staticdata, dtime_s)
self.object:set_armor_groups({ immortal = 1 })
function ARROW_ENTITY.on_activate(self, staticdata, dtime_s)
self._time_in_air = 1.0 self._time_in_air = 1.0
local data = minetest.deserialize(staticdata) local data = minetest.deserialize(staticdata)
if data then if not data then return end
self._stuck = data.stuck
if data.stuck then -- Restore arrow state
if data.stuckstarttime then local save_fields = self._save_fields
-- First, check if the stuck arrow is aleady past its life timer. for i = 1,#save_fields do
-- If yes, delete it. local field = save_fields[i]
self._stucktimer = minetest.get_gametime() - data.stuckstarttime self["_"..field] = data[field]
if self._stucktimer > ARROW_TIMEOUT then
mcl_burning.extinguish(self.object)
self.object:remove()
return
end
end end
-- Perform a stuck recheck on the next step. if not self._vl_projectile then
self._stuckrechecktimer = STUCK_RECHECK_TIME self._vl_projetile = {}
self._stuckin = data.stuckin
end end
-- Get the remaining arrow state
self._lastpos = data.lastpos
self._startpos = data.startpos
self._damage = data.damage
self._is_critical = data.is_critical
if data.shootername then if data.shootername then
local shooter = minetest.get_player_by_name(data.shootername) local shooter = minetest.get_player_by_name(data.shootername)
if shooter and shooter:is_player() then if shooter and shooter:is_player() then
@ -513,11 +191,16 @@ function ARROW_ENTITY.on_activate(self, staticdata, dtime_s)
end end
if data.stuckin_player then if data.stuckin_player then
self._removed = true
self.object:remove() self.object:remove()
end end
end end,
self.object:set_armor_groups({ immortal = 1 }) }
end
-- Make the arrow entity available to other mods as a template
mcl_bows.arrow_entity = table.copy(arrow_entity)
vl_projectile.register("mcl_bows:arrow_entity", arrow_entity)
minetest.register_on_respawnplayer(function(player) minetest.register_on_respawnplayer(function(player)
for _, obj in pairs(player:get_children()) do for _, obj in pairs(player:get_children()) do
@ -528,8 +211,6 @@ minetest.register_on_respawnplayer(function(player)
end end
end) end)
minetest.register_entity("mcl_bows:arrow_entity", ARROW_ENTITY)
if minetest.get_modpath("mcl_core") and minetest.get_modpath("mcl_mobitems") then if minetest.get_modpath("mcl_core") and minetest.get_modpath("mcl_mobitems") then
minetest.register_craft({ minetest.register_craft({
output = "mcl_bows:arrow 4", output = "mcl_bows:arrow 4",

View File

@ -1,12 +1,5 @@
local S = minetest.get_translator(minetest.get_current_modname()) local S = minetest.get_translator(minetest.get_current_modname())
mcl_bows = {}
-- local arrows = {
-- ["mcl_bows:arrow"] = "mcl_bows:arrow_entity",
-- }
local GRAVITY = 9.81
local BOW_DURABILITY = 385 local BOW_DURABILITY = 385
-- Charging time in microseconds -- Charging time in microseconds
@ -43,13 +36,16 @@ mcl_fovapi.register_modifier({
}) })
function mcl_bows.shoot_arrow(arrow_item, pos, dir, yaw, shooter, power, damage, is_critical, bow_stack, collectable) function mcl_bows.shoot_arrow(arrow_item, pos, dir, yaw, shooter, power, damage, is_critical, bow_stack, collectable)
local obj = minetest.add_entity({x=pos.x,y=pos.y,z=pos.z}, arrow_item.."_entity") power = power or BOW_MAX_SPEED
if power == nil then damage = damage or 3
power = BOW_MAX_SPEED --19
end local obj = vl_projectile.create(arrow_item.."_entity", {
if damage == nil then pos = pos,
damage = 3 dir = dir,
end velocity = power,
owner = shooter,
})
local knockback local knockback
if bow_stack then if bow_stack then
local enchantments = mcl_enchanting.get_enchantments(bow_stack) local enchantments = mcl_enchanting.get_enchantments(bow_stack)
@ -65,23 +61,24 @@ function mcl_bows.shoot_arrow(arrow_item, pos, dir, yaw, shooter, power, damage,
mcl_burning.set_on_fire(obj, math.huge) mcl_burning.set_on_fire(obj, math.huge)
end end
end end
obj:set_velocity({x=dir.x*power, y=dir.y*power, z=dir.z*power})
obj:set_acceleration({x=0, y=-GRAVITY, z=0})
obj:set_yaw(yaw-math.pi/2)
local le = obj:get_luaentity() local le = obj:get_luaentity()
le._shooter = shooter
le._source_object = shooter le._source_object = shooter
le._damage = damage le._damage = damage
le._is_critical = is_critical le._is_critical = is_critical
le._startpos = pos le._startpos = pos
le._knockback = knockback le._knockback = knockback
le._collectable = collectable le._collectable = collectable
le._arrow_item = arrow_item
local item_def = minetest.registered_items[le._arrow_item]
if item_def and item_def._arrow_image then
obj:set_properties({ textures = item_def._arrow_image })
end
minetest.sound_play("mcl_bows_bow_shoot", {pos=pos, max_hear_distance=16}, true) minetest.sound_play("mcl_bows_bow_shoot", {pos=pos, max_hear_distance=16}, true)
if shooter and shooter:is_player() then if shooter and shooter:is_player() then
if obj:get_luaentity().player == "" then if le.player == "" then
obj:get_luaentity().player = shooter le.player = shooter
end end
obj:get_luaentity().node = shooter:get_inventory():get_stack("main", 1):get_name() le.node = shooter:get_inventory():get_stack("main", 1):get_name()
end end
return obj return obj
end end

View File

@ -6,7 +6,6 @@ mcl_bows_s = {}
-- ["mcl_bows:arrow"] = "mcl_bows:arrow_entity", -- ["mcl_bows:arrow"] = "mcl_bows:arrow_entity",
-- } -- }
local GRAVITY = 9.81
local BOW_DURABILITY = 385 local BOW_DURABILITY = 385
-- Charging time in microseconds -- Charging time in microseconds
@ -41,13 +40,15 @@ local bow_load = {}
local bow_index = {} local bow_index = {}
function mcl_bows_s.shoot_arrow_crossbow(arrow_item, pos, dir, yaw, shooter, power, damage, is_critical, crossbow_stack, collectable) function mcl_bows_s.shoot_arrow_crossbow(arrow_item, pos, dir, yaw, shooter, power, damage, is_critical, crossbow_stack, collectable)
local obj = minetest.add_entity({x=pos.x,y=pos.y,z=pos.z}, arrow_item.."_entity") power = power or BOW_MAX_SPEED
if power == nil then damage = damage or 3
power = BOW_MAX_SPEED --19
end local obj = vl_projectile.create(arrow_item.."_entity", {
if damage == nil then pos = pos,
damage = 3 dir = dir,
end velocity = power,
owner = shooter,
})
local knockback = 4.875 local knockback = 4.875
if crossbow_stack then if crossbow_stack then
local enchantments = mcl_enchanting.get_enchantments(crossbow_stack) local enchantments = mcl_enchanting.get_enchantments(crossbow_stack)
@ -57,17 +58,14 @@ function mcl_bows_s.shoot_arrow_crossbow(arrow_item, pos, dir, yaw, shooter, pow
obj:get_luaentity()._piercing = 0 obj:get_luaentity()._piercing = 0
end end
end end
obj:set_velocity({x=dir.x*power, y=dir.y*power, z=dir.z*power})
obj:set_acceleration({x=0, y=-GRAVITY, z=0})
obj:set_yaw(yaw-math.pi/2)
local le = obj:get_luaentity() local le = obj:get_luaentity()
le._shooter = shooter
le._source_object = shooter le._source_object = shooter
le._damage = damage le._damage = damage
le._is_critical = is_critical le._is_critical = is_critical
le._startpos = pos le._startpos = pos
le._knockback = knockback le._knockback = knockback
le._collectable = collectable le._collectable = collectable
le._arrow_item = arrow_item
minetest.sound_play("mcl_bows_crossbow_shoot", {pos=pos, max_hear_distance=16}, true) minetest.sound_play("mcl_bows_crossbow_shoot", {pos=pos, max_hear_distance=16}, true)
if shooter and shooter:is_player() then if shooter and shooter:is_player() then
if obj:get_luaentity().player == "" then if obj:get_luaentity().player == "" then
@ -302,21 +300,6 @@ controls.register_on_press(function(player, key, time)
if wielditem:get_name()=="mcl_bows:crossbow_loaded" or wielditem:get_name()=="mcl_bows:crossbow_loaded_enchanted" then if wielditem:get_name()=="mcl_bows:crossbow_loaded" or wielditem:get_name()=="mcl_bows:crossbow_loaded_enchanted" then
local enchanted = mcl_enchanting.is_enchanted(wielditem:get_name()) local enchanted = mcl_enchanting.is_enchanted(wielditem:get_name())
local speed, damage local speed, damage
local p_load = bow_load[player:get_player_name()]
local charge
-- Type sanity check
if type(p_load) == "number" then
charge = minetest.get_us_time() - p_load
else
-- In case something goes wrong ...
-- Just assume minimum charge.
charge = 0
minetest.log("warning", "[mcl_bows] Player "..player:get_player_name().." fires arrow with non-numeric bow_load!")
end
charge = math.max(math.min(charge, BOW_CHARGE_TIME_FULL), 0)
local charge_ratio = charge / BOW_CHARGE_TIME_FULL
charge_ratio = math.max(math.min(charge_ratio, 1), 0)
-- Calculate damage and speed -- Calculate damage and speed
-- Fully charged -- Fully charged

View File

@ -1,3 +1,5 @@
mcl_bows = {}
--Bow --Bow
dofile(minetest.get_modpath("mcl_bows") .. "/arrow.lua") dofile(minetest.get_modpath("mcl_bows") .. "/arrow.lua")
dofile(minetest.get_modpath("mcl_bows") .. "/bow.lua") dofile(minetest.get_modpath("mcl_bows") .. "/bow.lua")

View File

@ -1,6 +1,6 @@
name = mcl_bows name = mcl_bows
author = Arcelmi author = Arcelmi
description = This mod adds bows and arrows for MineClone 2. description = This mod adds bows and arrows for MineClone 2.
depends = controls, mcl_particles, mcl_enchanting, mcl_init, mcl_util, mcl_shields, mcl_fovapi, mcl_luck depends = controls, mcl_particles, mcl_enchanting, mcl_init, mcl_util, mcl_shields, mcl_fovapi, mcl_luck, vl_projectile, mcl_explosions
optional_depends = awards, mcl_achievements, mcl_core, mcl_mobitems, playerphysics, doc, doc_identifier, mesecons_button optional_depends = awards, mcl_achievements, mcl_core, mcl_mobitems, playerphysics, doc, doc_identifier, mesecons_button

View File

@ -4,23 +4,14 @@ local math = math
local vector = vector local vector = vector
-- Time in seconds after which a stuck arrow is deleted -- Time in seconds after which a stuck arrow is deleted
local ARROW_TIMEOUT = 1 local ROCKET_TIMEOUT = 1
-- Time after which stuck arrow is rechecked for being stuck
local STUCK_RECHECK_TIME = 0.1
--local GRAVITY = 9.81
local YAW_OFFSET = -math.pi/2 local YAW_OFFSET = -math.pi/2
local function dir_to_pitch(dir) local function damage_explosion(self, damagemulitplier, pos)
--local dir2 = vector.normalize(dir)
local xz = math.abs(dir.x) + math.abs(dir.z)
return -math.atan2(-dir.y, xz)
end
local function damage_explosion(self, damagemulitplier)
if self._harmless then return end if self._harmless then return end
local p = self.object:get_pos()
local p = pos or self.object:get_pos()
if not p then return end if not p then return end
mcl_explosions.explode(p, 3, {}) mcl_explosions.explode(p, 3, {})
local objects = minetest.get_objects_inside_radius(p, 8) local objects = minetest.get_objects_inside_radius(p, 8)
@ -36,10 +27,10 @@ local function damage_explosion(self, damagemulitplier)
end end
end end
local function particle_explosion(self) local function particle_explosion(pos)
if pos.object then pos = pos.object:get_pos() end
local particle_pattern = math.random(1, 3) local particle_pattern = math.random(1, 3)
local fpitch local fpitch
--local true_type
local type = math.random(1, 2) local type = math.random(1, 2)
local size = math.random(1, 3) local size = math.random(1, 3)
local colors = {"red", "yellow", "blue", "green", "white"} local colors = {"red", "yellow", "blue", "green", "white"}
@ -53,22 +44,16 @@ local function particle_explosion(self)
fpitch = math.random(60, 70) fpitch = math.random(60, 70)
end end
--[[if type == 1 then
true_type = "Popper"
else
true_type = "Floof"
end]]
if type == 1 then if type == 1 then
minetest.sound_play("mcl_bows_firework", { minetest.sound_play("mcl_bows_firework", {
pos = self.object:get_pos(), pos = pos,
max_hear_distance = 100, max_hear_distance = 100,
gain = 3.0, gain = 3.0,
pitch = fpitch/100 pitch = fpitch/100
}, true) }, true)
else else
minetest.sound_play("mcl_bows_firework_soft", { minetest.sound_play("mcl_bows_firework_soft", {
pos = self.object:get_pos(), pos = pos,
max_hear_distance = 100, max_hear_distance = 100,
gain = 4.0, gain = 4.0,
pitch = fpitch/100 pitch = fpitch/100
@ -79,8 +64,8 @@ local function particle_explosion(self)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 400 * size, amount = 400 * size,
time = 0.0001, time = 0.0001,
minpos = self.object:get_pos(), minpos = pos,
maxpos = self.object:get_pos(), maxpos = pos,
minvel = vector.new(-7 * size,-7 * size,-7 * size), minvel = vector.new(-7 * size,-7 * size,-7 * size),
maxvel = vector.new(7 * size,7 * size,7 * size), maxvel = vector.new(7 * size,7 * size,7 * size),
minexptime = .6 * size / 2, minexptime = .6 * size / 2,
@ -95,8 +80,8 @@ local function particle_explosion(self)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 400 * size, amount = 400 * size,
time = 0.0001, time = 0.0001,
minpos = self.object:get_pos(), minpos = pos,
maxpos = self.object:get_pos(), maxpos = pos,
minvel = vector.new(-2 * size,-2 * size,-2 * size), minvel = vector.new(-2 * size,-2 * size,-2 * size),
maxvel = vector.new(2 * size,2 * size,2 * size), maxvel = vector.new(2 * size,2 * size,2 * size),
minexptime = .6 * size / 2, minexptime = .6 * size / 2,
@ -111,8 +96,8 @@ local function particle_explosion(self)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 100 * size, amount = 100 * size,
time = 0.0001, time = 0.0001,
minpos = self.object:get_pos(), minpos = pos,
maxpos = self.object:get_pos(), maxpos = pos,
minvel = vector.new(-14 * size,-14 * size,-14 * size), minvel = vector.new(-14 * size,-14 * size,-14 * size),
maxvel = vector.new(14 * size,14 * size,14 * size), maxvel = vector.new(14 * size,14 * size,14 * size),
minexptime = .6 * size / 2, minexptime = .6 * size / 2,
@ -129,8 +114,8 @@ local function particle_explosion(self)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 240 * size, amount = 240 * size,
time = 0.0001, time = 0.0001,
minpos = self.object:get_pos(), minpos = pos,
maxpos = self.object:get_pos(), maxpos = pos,
minvel = vector.new(-5 * size,-5 * size,-5 * size), minvel = vector.new(-5 * size,-5 * size,-5 * size),
maxvel = vector.new(5 * size,5 * size,5 * size), maxvel = vector.new(5 * size,5 * size,5 * size),
minexptime = .6 * size / 2, minexptime = .6 * size / 2,
@ -145,8 +130,8 @@ local function particle_explosion(self)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 500 * size, amount = 500 * size,
time = 0.0001, time = 0.0001,
minpos = self.object:get_pos(), minpos = pos,
maxpos = self.object:get_pos(), maxpos = pos,
minvel = vector.new(-2 * size,-2 * size,-2 * size), minvel = vector.new(-2 * size,-2 * size,-2 * size),
maxvel = vector.new(2 * size,2 * size,2 * size), maxvel = vector.new(2 * size,2 * size,2 * size),
minexptime = .6 * size / 2, minexptime = .6 * size / 2,
@ -161,8 +146,8 @@ local function particle_explosion(self)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 350 * size, amount = 350 * size,
time = 0.0001, time = 0.0001,
minpos = self.object:get_pos(), minpos = pos,
maxpos = self.object:get_pos(), maxpos = pos,
minvel = vector.new(-3 * size,-3 * size,-3 * size), minvel = vector.new(-3 * size,-3 * size,-3 * size),
maxvel = vector.new(3 * size,3 * size,3 * size), maxvel = vector.new(3 * size,3 * size,3 * size),
minexptime = .6 * size / 2, minexptime = .6 * size / 2,
@ -179,8 +164,8 @@ local function particle_explosion(self)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 400 * size, amount = 400 * size,
time = 0.0001, time = 0.0001,
minpos = self.object:get_pos(), minpos = pos,
maxpos = self.object:get_pos(), maxpos = pos,
minvel = vector.new(-6 * size,-4 * size,-6 * size), minvel = vector.new(-6 * size,-4 * size,-6 * size),
maxvel = vector.new(6 * size,4 * size,6 * size), maxvel = vector.new(6 * size,4 * size,6 * size),
minexptime = .6 * size, minexptime = .6 * size,
@ -195,8 +180,8 @@ local function particle_explosion(self)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 120 * size, amount = 120 * size,
time = 0.0001, time = 0.0001,
minpos = self.object:get_pos(), minpos = pos,
maxpos = self.object:get_pos(), maxpos = pos,
minvel = vector.new(-8 * size,6 * size,-8 * size), minvel = vector.new(-8 * size,6 * size,-8 * size),
maxvel = vector.new(8 * size,6 * size,8 * size), maxvel = vector.new(8 * size,6 * size,8 * size),
minexptime = .6 * size, minexptime = .6 * size,
@ -211,8 +196,8 @@ local function particle_explosion(self)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 130 * size, amount = 130 * size,
time = 0.0001, time = 0.0001,
minpos = self.object:get_pos(), minpos = pos,
maxpos = self.object:get_pos(), maxpos = pos,
minvel = vector.new(-3 * size,3 * size,-3 * size), minvel = vector.new(-3 * size,3 * size,-3 * size),
maxvel = vector.new(3 * size,3 * size,3 * size), maxvel = vector.new(3 * size,3 * size,3 * size),
minexptime = .6 * size, minexptime = .6 * size,
@ -230,17 +215,12 @@ local function particle_explosion(self)
end end
local mod_awards = minetest.get_modpath("awards") and minetest.get_modpath("mcl_achievements")
local mod_button = minetest.get_modpath("mesecons_button")
local mod_target = minetest.get_modpath("mcl_target")
local enable_pvp = minetest.settings:get_bool("enable_pvp")
minetest.register_craftitem("mcl_bows:rocket", { minetest.register_craftitem("mcl_bows:rocket", {
description = S("Arrow"), description = S("Arrow"),
_tt_help = S("Ammunition").."\n"..S("Damage from bow: 1-10").."\n"..S("Damage from dispenser: 3"), _tt_help = S("Ammunition").."\n"..S("Damage from bow: 1-10").."\n"..S("Damage from dispenser: 3"),
_doc_items_longdesc = S("Arrows are ammunition for bows and dispensers.").."\n".. _doc_items_longdesc = S("Arrows are ammunition for bows and dispensers.").."\n"..
S("An arrow fired from a bow has a regular damage of 1-9. At full charge, there's a 20% chance of a critical hit dealing 10 damage instead. An arrow fired from a dispenser always deals 3 damage.").."\n".. S("An arrow fired from a bow has a regular damage of 1-9. At full charge, there's a 20% chance of a critical hit dealing 10 damage instead. An arrow fired from a dispenser always deals 3 damage.").."\n"..
S("Arrows might get stuck on solid blocks and can be retrieved again. They are also capable of pushing wooden buttons."), S("Arrows might get stuck on solid blocks and can be retrieved again. They are also capable of pushing wooden buttons."),
_doc_items_usagehelp = S("To use arrows as ammunition for a bow, just put them anywhere in your inventory, they will be used up automatically. To use arrows as ammunition for a dispenser, place them in the dispenser's inventory. To retrieve an arrow that sticks in a block, simply walk close to it."), _doc_items_usagehelp = S("To use arrows as ammunition for a bow, just put them anywhere in your inventory, they will be used up automatically. To use arrows as ammunition for a dispenser, place them in the dispenser's inventory. To retrieve an arrow that sticks in a block, simply walk close to it."),
inventory_image = "mcl_bows_rocket.png", inventory_image = "mcl_bows_rocket.png",
groups = { ammo=1, ammo_crossbow=1, ammo_bow_regular=1 }, groups = { ammo=1, ammo_crossbow=1, ammo_bow_regular=1 },
@ -250,448 +230,53 @@ S("Arrows might get stuck on solid blocks and can be retrieved again. They are a
local yaw = math.atan2(dropdir.z, dropdir.x) + YAW_OFFSET local yaw = math.atan2(dropdir.z, dropdir.x) + YAW_OFFSET
mcl_bows.shoot_arrow(itemstack:get_name(), shootpos, dropdir, yaw, nil, 19, 3) mcl_bows.shoot_arrow(itemstack:get_name(), shootpos, dropdir, yaw, nil, 19, 3)
end, end,
}) _on_collide_with_entity = function(self, _, obj)
local ARROW_ENTITY={
physical = true,
pointable = false,
visual = "mesh",
mesh = "mcl_bows_rocket.obj",
visual_size = {x=2.5, y=2.5},
textures = {"mcl_bows_rocket.png"},
collisionbox = {-0.19, -0.125, -0.19, 0.19, 0.125, 0.19},
collide_with_objects = false,
_fire_damage_resistant = true,
_lastpos={},
_startpos=nil,
_damage=1, -- Damage on impact
_is_critical=false, -- Whether this arrow would deal critical damage
_stuck=false, -- Whether arrow is stuck
_fuse=nil,-- Amount of time (in seconds) the arrow has been stuck so far
_fuserechecktimer=nil,-- An additional timer for periodically re-checking the stuck status of an arrow
_stuckin=nil, --Position of node in which arow is stuck.
_shooter=nil, -- ObjectRef of player or mob who shot it
_is_arrow = true,
_viscosity=0, -- Viscosity of node the arrow is currently in
_deflection_cooloff=0, -- Cooloff timer after an arrow deflection, to prevent many deflections in quick succession
}
-- Destroy arrow entity self at pos and drops it as an item
local function spawn_item(self, pos)
if not minetest.is_creative_enabled("") then
local item = minetest.add_item(pos, "mcl_bows:rocket")
item:set_velocity({x=0, y=0, z=0})
item:set_yaw(self.object:get_yaw())
end
mcl_burning.extinguish(self.object)
self.object:remove()
end
local function damage_particles(pos, is_critical)
if is_critical then
minetest.add_particlespawner({
amount = 15,
time = 0.1,
minpos = {x=pos.x-0.5, y=pos.y-0.5, z=pos.z-0.5},
maxpos = {x=pos.x+0.5, y=pos.y+0.5, z=pos.z+0.5},
minvel = {x=-0.1, y=-0.1, z=-0.1},
maxvel = {x=0.1, y=0.1, z=0.1},
minacc = {x=0, y=0, z=0},
maxacc = {x=0, y=0, z=0},
minexptime = 1,
maxexptime = 2,
minsize = 1.5,
maxsize = 1.5,
collisiondetection = false,
vertical = false,
texture = "mcl_particles_crit.png^[colorize:#bc7a57:127",
})
end
end
function ARROW_ENTITY.on_step(self, dtime)
mcl_burning.tick(self.object, dtime, self)
-- mcl_burning.tick may remove object immediately
if not self.object:get_pos() then return end
self._time_in_air = self._time_in_air + .001
local pos = self.object:get_pos()
local dpos = table.copy(pos) -- digital pos
dpos = vector.round(dpos)
local node = minetest.get_node(dpos)
if not self._fuse then
self._fuse = 0
end
if not self._fuserechecktimer then
self._fuserechecktimer = 0
end
self._fuse = self._fuse + dtime
self._fuserechecktimer = self._fuserechecktimer + dtime
if self._fuse > ARROW_TIMEOUT then
self._stuck = true
end
if self._stuck then
if self._fuse > ARROW_TIMEOUT then
local eploded_particle = particle_explosion(self)
damage_explosion(self, eploded_particle * 17)
mcl_burning.extinguish(self.object)
self.object:remove()
return
end
-- Drop arrow as item when it is no longer stuck
-- FIXME: Arrows are a bit slow to react and continue to float in mid air for a few seconds.
if self._fuserechecktimer > STUCK_RECHECK_TIME then
local stuckin_def
if self._stuckin then
stuckin_def = minetest.registered_nodes[minetest.get_node(self._stuckin).name]
end
-- TODO: In MC, arrow just falls down without turning into an item
if stuckin_def and stuckin_def.walkable == false then
spawn_item(self, pos)
return
end
self._fuserechecktimer = 0
end
-- Pickup arrow if player is nearby (not in Creative Mode)
local objects = minetest.get_objects_inside_radius(pos, 1)
for _,obj in ipairs(objects) do
if obj:is_player() then
if self._collectable and not minetest.is_creative_enabled(obj:get_player_name()) then
if obj:get_inventory():room_for_item("main", "mcl_bows:rocket") then
obj:get_inventory():add_item("main", "mcl_bows:rocket")
minetest.sound_play("item_drop_pickup", {
pos = pos,
max_hear_distance = 16,
gain = 1.0,
}, true)
end
end
mcl_burning.extinguish(self.object)
self.object:remove()
return
end
end
-- Check for object "collision". Done every tick (hopefully this is not too stressing)
else
if self._in_player == false then
minetest.add_particlespawner({
amount = 1,
time = .0001,
minpos = pos,
maxpos = pos,
minvel = vector.new(-0.1,-0.1,-0.1),
maxvel = vector.new(0.1,0.1,0.1),
minexptime = 0.5,
maxexptime = 0.5,
minsize = 2,
maxsize = 2,
collisiondetection = false,
vertical = false,
texture = "mcl_bows_rocket_particle.png",
glow = 1,
})
end
-- We just check for any hurtable objects nearby.
-- The radius of 3 is fairly liberal, but anything lower than than will cause
-- arrow to hilariously go through mobs often.
-- TODO: Implement an ACTUAL collision detection (engine support needed).
local objs = minetest.get_objects_inside_radius(pos, 1.5)
local closest_object
local closest_distance
if self._deflection_cooloff > 0 then
self._deflection_cooloff = self._deflection_cooloff - dtime
end
-- Iterate through all objects and remember the closest attackable object
local arrow_dir = self.object:get_velocity()
--create a raycast from the arrow based on the velocity of the arrow to deal with lag
local raycast = minetest.raycast(pos, vector.add(pos, vector.multiply(arrow_dir, 0.1)), true, false)
for hitpoint in raycast do
if hitpoint.type == "object" then
-- find the closest object that is in the way of the arrow
local ok = false
if hitpoint.ref:is_player() and enable_pvp then
ok = true
elseif not hitpoint.ref:is_player() and hitpoint.ref:get_luaentity() then
if (hitpoint.ref:get_luaentity().is_mob or hitpoint.ref:get_luaentity()._hittable_by_projectile) then
ok = true
end
end
if ok then
local dist = vector.distance(hitpoint.ref:get_pos(), pos)
if not closest_object or not closest_distance then
closest_object = hitpoint.ref
closest_distance = dist
elseif dist < closest_distance then
closest_object = hitpoint.ref
closest_distance = dist
end
end
end
end
-- If an attackable object was found, we will damage the closest one only
if closest_object then
local obj = closest_object
local is_player = obj:is_player()
local lua = obj:get_luaentity()
if obj == self._shooter and self._time_in_air > 1.02 or obj ~= self._shooter and (is_player or (lua and (lua.is_mob or lua._hittable_by_projectile))) then
if obj:get_hp() > 0 then
-- Check if there is no solid node between arrow and object
local ray = minetest.raycast(self.object:get_pos(), obj:get_pos(), true)
for pointed_thing in ray do
if pointed_thing.type == "object" and pointed_thing.ref == closest_object then
-- Target reached! We can proceed now.
break
elseif pointed_thing.type == "node" then
local nn = minetest.get_node(minetest.get_pointed_thing_position(pointed_thing)).name
local def = minetest.registered_nodes[nn]
if (not def) or def.walkable then
-- There's a node in the way. Delete arrow without damage
mcl_burning.extinguish(self.object)
self.object:remove()
return
end
end
end
-- Punch target object but avoid hurting enderman.
if not lua or lua.name ~= "mobs_mc:rover" then
if self._in_player == false then
damage_particles(self.object:get_pos(), self._is_critical)
end
if mcl_burning.is_burning(self.object) then
mcl_burning.set_on_fire(obj, 5)
end
if self._in_player == false then if self._in_player == false then
pos = self.object:get_pos()
obj:punch(self.object, 1.0, { obj:punch(self.object, 1.0, {
full_punch_interval=1.0, full_punch_interval=1.0,
damage_groups={fleshy=self._damage}, damage_groups={fleshy=self._damage},
}, self.object:get_velocity()) }, self.object:get_velocity())
if obj:is_player() then
local eploded_particle = particle_explosion(self) local eploded_particle = particle_explosion(pos)
damage_explosion(self, eploded_particle * 17) damage_explosion(self, eploded_particle * 17, pos)
mcl_burning.extinguish(self.object) mcl_burning.extinguish(self.object)
self.object:remove() self.object:remove()
end end
end end,
end })
local arrow_entity = mcl_bows.arrow_entity
local rocket_entity = table.copy(arrow_entity)
table.update(rocket_entity,{
mesh = "mcl_bows_rocket.obj",
textures = {"mcl_bows_rocket.png"},
visual_size = {x=2.5, y=2.5},
save_fields = {
"stuck", "fuse", "stuckin", "lastpos", "startpos", "damage", "is_critical", "shootername",
},
_fuse=nil,-- Amount of time (in seconds) the arrow has been stuck so far
_fuserechecktimer=nil,-- An additional timer for periodically re-checking the stuck status of an arrow
})
rocket_entity.on_step = function(self, dtime)
self._fuse = (self._fuse or 0) + dtime
if is_player then if self._fuse > ROCKET_TIMEOUT then
if self._shooter and self._shooter:is_player() and self._in_player == false then
-- “Ding” sound for hitting another player
minetest.sound_play({name="mcl_bows_hit_player", gain=0.1}, {to_player=self._shooter:get_player_name()}, true)
end
end
if lua then
local entity_name = lua.name
-- Achievement for hitting skeleton, wither skeleton or stray (TODO) with an arrow at least 50 meters away
-- NOTE: Range has been reduced because mobs unload much earlier than that ... >_>
-- TODO: This achievement should be given for the kill, not just a hit
if self._shooter and self._shooter:is_player() and vector.distance(pos, self._startpos) >= 20 then
if mod_awards and (entity_name == "mobs_mc:skeleton" or entity_name == "mobs_mc:stray" or entity_name == "mobs_mc:witherskeleton") then
awards.unlock(self._shooter:get_player_name(), "mcl:snipeSkeleton")
end
end
end
if self._in_player == false then
minetest.sound_play({name="mcl_bows_hit_other", gain=0.3}, {pos=self.object:get_pos(), max_hear_distance=16}, true)
end
end
if not obj:is_player() then
mcl_burning.extinguish(self.object)
if self._piercing == 0 then
local eploded_particle = particle_explosion(self)
damage_explosion(self, eploded_particle * 17)
self.object:remove()
end
end
return
end
end
end
-- Check for node collision
if self._lastpos.x~=nil and not self._stuck then
local def = minetest.registered_nodes[node.name]
local vel = self.object:get_velocity()
-- Arrow has stopped in one axis, so it probably hit something.
-- This detection is a bit clunky, but sadly, MT does not offer a direct collision detection for us. :-(
if (math.abs(vel.x) < 0.0001) or (math.abs(vel.z) < 0.0001) or (math.abs(vel.y) < 0.00001) then
-- Check for the node to which the arrow is pointing
local dir
if math.abs(vel.y) < 0.00001 then
if self._lastpos.y < pos.y then
dir = {x=0, y=1, z=0}
else
dir = {x=0, y=-1, z=0}
end
else
dir = minetest.facedir_to_dir(minetest.dir_to_facedir(minetest.yaw_to_dir(self.object:get_yaw()-YAW_OFFSET)))
end
self._stuckin = vector.add(dpos, dir)
local snode = minetest.get_node(self._stuckin)
local sdef = minetest.registered_nodes[snode.name]
-- If node is non-walkable, unknown or ignore, don't make arrow stuck.
-- This causes a deflection in the engine.
if not sdef or sdef.walkable == false or snode.name == "ignore" then
self._stuckin = nil
if self._deflection_cooloff <= 0 then
-- Lose 1/3 of velocity on deflection
local newvel = vector.multiply(vel, 0.6667)
self.object:set_velocity(newvel)
-- Reset deflection cooloff timer to prevent many deflections happening in quick succession
self._deflection_cooloff = 1.0
end
else
-- Node was walkable, make arrow stuck
self._stuck = true self._stuck = true
self._fuserechecktimer = 0
self.object:set_velocity({x=0, y=0, z=0})
self.object:set_acceleration({x=0, y=0, z=0})
minetest.sound_play({name="mcl_bows_hit_other", gain=0.3}, {pos=self.object:get_pos(), max_hear_distance=16}, true)
if mcl_burning.is_burning(self.object) and snode.name == "mcl_tnt:tnt" then
tnt.ignite(self._stuckin)
end end
if self._stuck and self._fuse > ROCKET_TIMEOUT then
-- Activate target local eploded_particle = particle_explosion(self)
if mod_target and snode.name == "mcl_target:target_off" then damage_explosion(self, eploded_particle * 17)
mcl_target.hit(self._stuckin, 1) --10 redstone ticks
end
-- Push the button! Push, push, push the button!
if mod_button and minetest.get_item_group(node.name, "button") > 0 and minetest.get_item_group(node.name, "button_push_by_arrow") == 1 then
local bdir = minetest.wallmounted_to_dir(node.param2)
-- Check the button orientation
if vector.equals(vector.add(dpos, bdir), self._stuckin) then
mesecon.push_button(dpos, node)
end
end
end
elseif (def and def.liquidtype ~= "none") then
-- Slow down arrow in liquids
local v = def.liquid_viscosity
if not v then
v = 0
end
--local old_v = self._viscosity
self._viscosity = v
local vpenalty = math.max(0.1, 0.98 - 0.1 * v)
if math.abs(vel.x) > 0.001 then
vel.x = vel.x * vpenalty
end
if math.abs(vel.z) > 0.001 then
vel.z = vel.z * vpenalty
end
self.object:set_velocity(vel)
end
end
-- Update yaw
if not self._stuck then
local vel = self.object:get_velocity()
local yaw = minetest.dir_to_yaw(vel)+YAW_OFFSET
local pitch = dir_to_pitch(vel)
self.object:set_rotation({ x = 0, y = yaw, z = pitch })
end
-- Update internal variable
self._lastpos={x=pos.x, y=pos.y, z=pos.z}
end
-- Force recheck of stuck arrows when punched.
-- Otherwise, punching has no effect.
function ARROW_ENTITY.on_punch(self)
if self._stuck then
self._fuserechecktimer = STUCK_RECHECK_TIME
end
end
function ARROW_ENTITY.get_staticdata(self)
local out = {
lastpos = self._lastpos,
startpos = self._startpos,
damage = self._damage,
is_critical = self._is_critical,
stuck = self._stuck,
stuckin = self._stuckin,
}
if self._stuck then
-- If _fuse is missing for some reason, assume the maximum
if not self._fuse then
self._fuse = ARROW_TIMEOUT
end
out.stuckstarttime = minetest.get_gametime() - self._fuse
end
if self._shooter and self._shooter:is_player() then
out.shootername = self._shooter:get_player_name()
end
return minetest.serialize(out)
end
function ARROW_ENTITY.on_activate(self, staticdata, dtime_s)
self._time_in_air = 1.0
self._in_player = false
local data = minetest.deserialize(staticdata)
if data then
self._stuck = data.stuck
if data.stuck then
if data.stuckstarttime then
-- First, check if the stuck arrow is aleady past its life timer.
-- If yes, delete it.
self._fuse = minetest.get_gametime() - data.stuckstarttime
if self._fuse > ARROW_TIMEOUT then
mcl_burning.extinguish(self.object) mcl_burning.extinguish(self.object)
self.object:remove() self.object:remove()
return return
end end
end
self._fuse = 2 -- Perform normal arrow behaviors
-- Perform a stuck recheck on the next step. arrow_entity.on_step(self, dtime)
self._fuserechecktimer = STUCK_RECHECK_TIME
self._stuckin = data.stuckin
end
-- Get the remaining arrow state
self._lastpos = data.lastpos
self._startpos = data.startpos
self._damage = data.damage
self._is_critical = data.is_critical
if data.shootername then
local shooter = minetest.get_player_by_name(data.shootername)
if shooter and shooter:is_player() then
self._shooter = shooter
end
end
end
self.object:set_armor_groups({ immortal = 1 })
end end
minetest.register_entity("mcl_bows:rocket_entity", ARROW_ENTITY) vl_projectile.register("mcl_bows:rocket_entity", rocket_entity)
if minetest.get_modpath("mcl_core") and minetest.get_modpath("mcl_mobitems") then if minetest.get_modpath("mcl_core") and minetest.get_modpath("mcl_mobitems") then
minetest.register_craft({ minetest.register_craft({

View File

@ -261,6 +261,14 @@ function mcl_campfires.register_campfire(name, def)
}, },
_mcl_blast_resistance = 2, _mcl_blast_resistance = 2,
_mcl_hardness = 2, _mcl_hardness = 2,
_vl_projectile = {
on_collide = function(projectile, pos, node, node_def)
-- Ignite Campfires
if mcl_burning.is_burning(projectile) then
mcl_campfires.light_campfire(pos)
end
end
},
after_dig_node = function(pos, node, oldmeta, digger) after_dig_node = function(pos, node, oldmeta, digger)
campfire_drops(pos, digger, def.drops, name.."_lit") campfire_drops(pos, digger, def.drops, name.."_lit")
end, end,

View File

@ -312,7 +312,7 @@ bobber_ENTITY.on_step = bobber_on_step
minetest.register_entity("mcl_fishing:bobber_entity", bobber_ENTITY) minetest.register_entity("mcl_fishing:bobber_entity", bobber_ENTITY)
local flying_bobber_ENTITY={ vl_projectile.register("mcl_fishing:flying_bobber_entity", {
physical = false, physical = false,
timer=0, timer=0,
textures = {"mcl_fishing_bobber.png"}, --FIXME: Replace with correct texture. textures = {"mcl_fishing_bobber.png"}, --FIXME: Replace with correct texture.
@ -323,35 +323,35 @@ local flying_bobber_ENTITY={
get_staticdata = mcl_throwing.get_staticdata, get_staticdata = mcl_throwing.get_staticdata,
on_activate = mcl_throwing.on_activate, on_activate = mcl_throwing.on_activate,
_vl_projectile = {
behaviors = {
vl_projectile.collides_with_solids,
},
collides_with = { "group:liquid" },
on_collide_with_solid = function(self, pos, node)
local player = self._owner
self._remove = true
self.object:remove()
-- Make sure the player field is valid for when we create the floating bobber
if not player then return end
local def = minetest.registered_nodes[node.name]
if not def then return end
if def.walkable or def.liquidtype == "flowing" or def.liquidtype == "source" then
local ent = minetest.add_entity(pos, "mcl_fishing:bobber_entity"):get_luaentity()
ent.player = player
ent.child = true
end
end
},
_lastpos={}, _lastpos={},
_thrower = nil, _thrower = nil,
objtype="fishing", objtype="fishing",
} })
-- Movement function of flying bobber
local function flying_bobber_on_step(self, dtime)
self.timer=self.timer+dtime
local pos = self.object:get_pos()
local node = minetest.get_node(pos)
local def = minetest.registered_nodes[node.name]
--local player = minetest.get_player_by_name(self._thrower)
-- Destroy when hitting a solid node
if self._lastpos.x~=nil then
if (def and (def.walkable or def.liquidtype == "flowing" or def.liquidtype == "source")) or not def then
local ent = minetest.add_entity(self._lastpos, "mcl_fishing:bobber_entity"):get_luaentity()
ent.player = self._thrower
ent.child = true
self.object:remove()
return
end
end
self._lastpos={x=pos.x, y=pos.y, z=pos.z} -- Set lastpos-->Node will be added at last pos outside the node
end
flying_bobber_ENTITY.on_step = flying_bobber_on_step
minetest.register_entity("mcl_fishing:flying_bobber_entity", flying_bobber_ENTITY)
mcl_throwing.register_throwable_object("mcl_fishing:flying_bobber", "mcl_fishing:flying_bobber_entity", 5) mcl_throwing.register_throwable_object("mcl_fishing:flying_bobber", "mcl_fishing:flying_bobber_entity", 5)

View File

@ -1,5 +1,6 @@
local S = minetest.get_translator(minetest.get_current_modname()) local S = minetest.get_translator(minetest.get_current_modname())
local PARTICLE_DENSITY = 4
local mod_target = minetest.get_modpath("mcl_target") local mod_target = minetest.get_modpath("mcl_target")
local function lingering_image(colorstring, opacity) local function lingering_image(colorstring, opacity)
@ -53,7 +54,7 @@ minetest.register_globalstep(function(dtime)
else else
texture = "mcl_particles_effect.png" texture = "mcl_particles_effect.png"
end end
linger_particles(pos, d, texture, vals.color) linger_particles(pos, PARTICLE_DENSITY, texture, vals.color)
-- -- Extinguish fire if water bottle -- -- Extinguish fire if water bottle
-- if vals.is_water then -- if vals.is_water then
@ -181,40 +182,46 @@ function mcl_potions.register_lingering(name, descr, color, def)
local w = 0.7 local w = 0.7
minetest.register_entity(id.."_flying",{ local particle_texture
if name == "water" then
particle_texture = "mcl_particles_droplet_bottle.png"
else
if def.instant then
particle_texture = "mcl_particles_instant_effect.png"
else
particle_texture = "mcl_particles_effect.png"
end
end
local function on_collide(self, pos)
local potency = self._potency or 0
local plus = self._plus or 0
add_lingering_effect(pos, color, def, name == "water", potency, plus)
linger_particles(pos, PARTICLE_DENSITY, particle_texture, color)
if def.on_splash then def.on_splash(pos, potency+1) end
end
vl_projectile.register(id.."_flying",{
textures = {lingering_image(color)}, textures = {lingering_image(color)},
hp_max = 1, hp_max = 1,
visual_size = {x=w/2,y=w/2}, visual_size = {x=w/2,y=w/2},
collisionbox = {-0.1,-0.1,-0.1,0.1,0.1,0.1}, collisionbox = {-0.1,-0.1,-0.1,0.1,0.1,0.1},
pointable = false, pointable = false,
on_step = function(self, dtime) _vl_projectile = {
local pos = self.object:get_pos() behaviors = {
local node = minetest.get_node(pos) vl_projectile.collides_with_entities,
local n = node.name vl_projectile.collides_with_solids,
local g = minetest.get_item_group(n, "liquid") },
local d = 4 on_collide_with_entity = on_collide,
if mod_target and n == "mcl_target:target_off" then on_collide_with_solid = function(self, pos, node)
if mod_target and node.name == "mcl_target:target_off" then
mcl_target.hit(vector.round(pos), 0.4) --4 redstone ticks mcl_target.hit(vector.round(pos), 0.4) --4 redstone ticks
end end
if n ~= "air" and n ~= "mcl_portals:portal" and n ~= "mcl_portals:portal_end" and g == 0 or mcl_potions.is_obj_hit(self, pos) then
minetest.sound_play("mcl_potions_breaking_glass", {pos = pos, max_hear_distance = 16, gain = 1}) on_collide(self, pos)
local potency = self._potency or 0
local plus = self._plus or 0
add_lingering_effect(pos, color, def, name == "water", potency, plus)
local texture
if name == "water" then
texture = "mcl_particles_droplet_bottle.png"
else
if def.instant then
texture = "mcl_particles_instant_effect.png"
else
texture = "mcl_particles_effect.png"
end
end
linger_particles(pos, d, texture, color)
if def.on_splash then def.on_splash(pos, potency+1) end
self.object:remove()
end
end, end,
sounds = {
on_collision = {"mcl_potions_breaking_glass", {max_hear_distance = 16, gain = 1}},
},
},
}) })
end end

View File

@ -1,2 +1,2 @@
name = mcl_potions name = mcl_potions
depends = mcl_core, mcl_farming, mcl_flowers, mcl_mobitems, mcl_mobs, mcl_fishing, mcl_bows, mcl_end, mcl_weather, playerphysics, mcl_wip depends = mcl_core, mcl_farming, mcl_flowers, mcl_mobitems, mcl_mobs, mcl_fishing, mcl_bows, mcl_end, mcl_weather, playerphysics, mcl_wip, vl_projectile

View File

@ -1,5 +1,9 @@
local S = minetest.get_translator(minetest.get_current_modname()) local S = minetest.get_translator(minetest.get_current_modname())
local GRAVITY = tonumber(minetest.settings:get("movement_gravity")) local GRAVITY = tonumber(minetest.settings:get("movement_gravity"))
local REDUX_MAP = {7/8,0.5,0.25}
local PARTICLE_DIAMETER = 0.1
local PARTICLE_MIN_VELOCITY = vector.new(-2, 0, -2)
local PARTICLE_MAX_VELOCITY = vector.new( 2, 2, 2)
local mod_target = minetest.get_modpath("mcl_target") local mod_target = minetest.get_modpath("mcl_target")
@ -10,7 +14,6 @@ local function splash_image(colorstring, opacity)
return "mcl_potions_splash_overlay.png^[colorize:"..colorstring..":"..tostring(opacity).."^mcl_potions_splash_bottle.png" return "mcl_potions_splash_overlay.png^[colorize:"..colorstring..":"..tostring(opacity).."^mcl_potions_splash_bottle.png"
end end
function mcl_potions.register_splash(name, descr, color, def) function mcl_potions.register_splash(name, descr, color, def)
local id = "mcl_potions:"..name.."_splash" local id = "mcl_potions:"..name.."_splash"
local longdesc = def._longdesc local longdesc = def._longdesc
@ -22,6 +25,7 @@ function mcl_potions.register_splash(name, descr, color, def)
end end
local groups = {brewitem=1, bottle=1, splash_potion=1, _mcl_potion=1} local groups = {brewitem=1, bottle=1, splash_potion=1, _mcl_potion=1}
if def.nocreative then groups.not_in_creative_inventory = 1 end if def.nocreative then groups.not_in_creative_inventory = 1 end
minetest.register_craftitem(id, { minetest.register_craftitem(id, {
description = descr, description = descr,
_tt_help = def._tt, _tt_help = def._tt,
@ -73,75 +77,64 @@ function mcl_potions.register_splash(name, descr, color, def)
local w = 0.7 local w = 0.7
minetest.register_entity(id.."_flying",{ -- Precompute particle texture and acceleration
textures = {splash_image(color)}, local particle_texture, particle_acc
hp_max = 1,
visual_size = {x=w/2,y=w/2},
collisionbox = {-0.1,-0.1,-0.1,0.1,0.1,0.1},
pointable = false,
on_step = function(self, dtime)
local pos = self.object:get_pos()
local node = minetest.get_node(pos)
local n = node.name
local g = minetest.get_item_group(n, "liquid")
local d = 0.1
local redux_map = {7/8,0.5,0.25}
if mod_target and n == "mcl_target:target_off" then
mcl_target.hit(vector.round(pos), 0.4) --4 redstone ticks
end
if n ~= "air" and n ~= "mcl_portals:portal" and n ~= "mcl_portals:portal_end" and g == 0 or mcl_potions.is_obj_hit(self, pos) then
minetest.sound_play("mcl_potions_breaking_glass", {pos = pos, max_hear_distance = 16, gain = 1})
local texture, acc
if name == "water" then if name == "water" then
texture = "mcl_particles_droplet_bottle.png" particle_texture = "mcl_particles_droplet_bottle.png"
acc = {x=0, y=-GRAVITY, z=0} particle_acc = {x=0, y=-GRAVITY, z=0}
else else
if def.instant then if def.instant then
texture = "mcl_particles_instant_effect.png" particle_texture = "mcl_particles_instant_effect.png"
else else
texture = "mcl_particles_effect.png" particle_texture = "mcl_particles_effect.png"
end end
acc = {x=0, y=0, z=0} particle_acc = {x=0, y=0, z=0}
end end
particle_texture = particle_texture.."^[colorize:"..color..":127"
local function splash_effects(self, pos, def, range)
minetest.add_particlespawner({ minetest.add_particlespawner({
amount = 50, amount = 50,
time = 0.1, time = 0.1,
minpos = {x=pos.x-d, y=pos.y+0.5, z=pos.z-d}, minpos = vector.offset(pos, -PARTICLE_DIAMETER, 0.5, -PARTICLE_DIAMETER),
maxpos = {x=pos.x+d, y=pos.y+0.5+d, z=pos.z+d}, maxpos = vector.offset(pos, PARTICLE_DIAMETER, 0.5, PARTICLE_DIAMETER),
minvel = {x=-2, y=0, z=-2}, minvel = PARTICLE_MIN_VELOCITY,
maxvel = {x=2, y=2, z=2}, maxvel = PARTICLE_MAX_VELOCITY,
minacc = acc, minacc = particle_acc,
maxacc = acc, maxacc = particle_acc,
minexptime = 0.5, minexptime = 0.5,
maxexptime = 1.25, maxexptime = 1.25,
minsize = 1, minsize = 1,
maxsize = 2, maxsize = 2,
collisiondetection = true, collisiondetection = true,
vertical = false, vertical = false,
texture = texture.."^[colorize:"..color..":127" texture = particle_texture,
}) })
for _,obj in pairs(minetest.get_objects_inside_radius(pos, range)) do
-- Make sure the potion can interact with this object
local entity = obj:get_luaentity()
if obj:is_player() or entity and entity.is_mob then
local potency = self._potency or 0 local potency = self._potency or 0
local plus = self._plus or 0 local plus = self._plus or 0
if def.on_splash then def.on_splash(pos, potency+1) end if def.on_splash then def.on_splash(pos, potency+1) end
for _,obj in pairs(minetest.get_objects_inside_radius(pos, 4)) do
local entity = obj:get_luaentity()
if obj:is_player() or entity and entity.is_mob then
local pos2 = obj:get_pos() local pos2 = obj:get_pos()
local rad = math.floor(math.sqrt((pos2.x-pos.x)^2 + (pos2.y-pos.y)^2 + (pos2.z-pos.z)^2)) local rad = math.floor(math.sqrt((pos2.x-pos.x)^2 + (pos2.y-pos.y)^2 + (pos2.z-pos.z)^2))
-- Apply effect list
if def._effect_list then if def._effect_list then
for name, details in pairs(def._effect_list) do
local ef_level local ef_level
local dur local dur
for name, details in pairs(def._effect_list) do
if details.uses_level then if details.uses_level then
ef_level = details.level + details.level_scaling * (potency) ef_level = details.level + details.level_scaling * potency
else else
ef_level = details.level ef_level = details.level
end end
if details.dur_variable then if details.dur_variable then
dur = details.dur * math.pow(mcl_potions.PLUS_FACTOR, plus) dur = details.dur * math.pow(mcl_potions.PLUS_FACTOR, plus)
if potency>0 and details.uses_level then if potency>0 and details.uses_level then
@ -151,11 +144,13 @@ function mcl_potions.register_splash(name, descr, color, def)
else else
dur = details.dur dur = details.dur
end end
if details.effect_stacks then if details.effect_stacks then
ef_level = ef_level + mcl_potions.get_effect_level(obj, name) ef_level = ef_level + mcl_potions.get_effect_level(obj, name)
end end
if rad > 0 then if rad > 0 then
mcl_potions.give_effect_by_level(name, obj, ef_level, redux_map[rad]*dur) mcl_potions.give_effect_by_level(name, obj, ef_level, REDUX_MAP[rad]*dur)
else else
mcl_potions.give_effect_by_level(name, obj, ef_level, dur) mcl_potions.give_effect_by_level(name, obj, ef_level, dur)
end end
@ -165,20 +160,40 @@ function mcl_potions.register_splash(name, descr, color, def)
if def.custom_effect then if def.custom_effect then
local power = (potency+1) * mcl_potions.SPLASH_FACTOR local power = (potency+1) * mcl_potions.SPLASH_FACTOR
if rad > 0 then if rad > 0 then
def.custom_effect(obj, redux_map[rad] * power, plus) def.custom_effect(obj, REDUX_MAP[rad] * power, plus)
else else
def.custom_effect(obj, power, plus) def.custom_effect(obj, power, plus)
end end
end end
end end
end end
self.object:remove() end
vl_projectile.register(id.."_flying",{
textures = {splash_image(color)},
hp_max = 1,
visual_size = {x=w/2,y=w/2},
collisionbox = {-0.1,-0.1,-0.1,0.1,0.1,0.1},
_vl_projectile = {
behaviors = {
vl_projectile.collides_with_entities,
vl_projectile.collides_with_solids,
},
on_collide_with_solid = function(self, pos, node)
splash_effects(self, pos, def, 4)
if mod_target and node.name == "mcl_target:target_off" then
mcl_target.hit(pos, 0.4) -- 4 redstone ticks
end end
end, end,
on_collide_with_entity = function(self, pos, obj)
splash_effects(self, pos, def, 4)
end,
sounds = {
on_collision = {"mcl_potions_breaking_glass", {max_hear_distance = 16, gain = 1}, true},
},
},
pointable = false,
}) })
end end
--[[local function time_string(dur)
return math.floor(dur/60)..string.format(":%02d",math.floor(dur % 60))
end]]

View File

@ -1,24 +1,9 @@
local S = minetest.get_translator(minetest.get_current_modname()) local S = minetest.get_translator(minetest.get_current_modname())
local mod_target = minetest.get_modpath("mcl_target")
local math = math local math = math
-- Time in seconds after which a stuck arrow is deleted
local ARROW_TIMEOUT = 60
-- Time after which stuck arrow is rechecked for being stuck
local STUCK_RECHECK_TIME = 5
--local GRAVITY = 9.81
local YAW_OFFSET = -math.pi/2 local YAW_OFFSET = -math.pi/2
local function dir_to_pitch(dir)
--local dir2 = vector.normalize(dir)
local xz = math.abs(dir.x) + math.abs(dir.z)
return -math.atan2(-dir.y, xz)
end
local function arrow_image(colorstring, opacity) local function arrow_image(colorstring, opacity)
if not opacity then if not opacity then
opacity = 127 opacity = 127
@ -28,20 +13,16 @@ end
local how_to_shoot = minetest.registered_items["mcl_bows:arrow"]._doc_items_usagehelp local how_to_shoot = minetest.registered_items["mcl_bows:arrow"]._doc_items_usagehelp
local mod_awards = minetest.get_modpath("awards") and minetest.get_modpath("mcl_achievements")
local mod_button = minetest.get_modpath("mesecons_button")
local enable_pvp = minetest.settings:get_bool("enable_pvp")
local arrow_longdesc = minetest.registered_items["mcl_bows:arrow"]._doc_items_longdesc or "" local arrow_longdesc = minetest.registered_items["mcl_bows:arrow"]._doc_items_longdesc or ""
local arrow_tt = minetest.registered_items["mcl_bows:arrow"]._tt_help or "" local arrow_tt = minetest.registered_items["mcl_bows:arrow"]._tt_help or ""
function mcl_potions.register_arrow(name, desc, color, def) function mcl_potions.register_arrow(name, desc, color, def)
local longdesc = def._longdesc or "" local longdesc = def._longdesc or ""
local tt = def._tt or "" local tt = def._tt or ""
local groups = {ammo=1, ammo_bow=1, brewitem=1, _mcl_potion=1} local groups = {ammo=1, ammo_bow=1, brewitem=1, _mcl_potion=1}
if def.nocreative then groups.not_in_creative_inventory = 1 end if def.nocreative then groups.not_in_creative_inventory = 1 end
minetest.register_craftitem("mcl_potions:"..name.."_arrow", { local arrow_item = "mcl_potions:"..name.."_arrow"
minetest.register_craftitem(arrow_item, {
description = desc, description = desc,
_tt_help = arrow_tt .. "\n" .. tt, _tt_help = arrow_tt .. "\n" .. tt,
_dynamic_tt = def._dynamic_tt, _dynamic_tt = def._dynamic_tt,
@ -63,245 +44,27 @@ function mcl_potions.register_arrow(name, desc, color, def)
local yaw = math.atan2(dropdir.z, dropdir.x) + YAW_OFFSET local yaw = math.atan2(dropdir.z, dropdir.x) + YAW_OFFSET
mcl_bows.shoot_arrow(itemstack:get_name(), shootpos, dropdir, yaw, nil, 19, 3) mcl_bows.shoot_arrow(itemstack:get_name(), shootpos, dropdir, yaw, nil, 19, 3)
end, end,
}) _arrow_image = arrow_image(color, 100),
_on_collide_with_entity = function(self, pos, obj)
-- This is a fake node, used as model for the arrow entity.
-- It's not supposed to be usable as item or real node.
-- TODO: Use a proper mesh for the arrow entity
minetest.register_node("mcl_potions:"..name.."_arrow_box", {
drawtype = "nodebox",
is_ground_content = false,
node_box = {
type = "fixed",
fixed = {
-- Shaft
{-6.5/17, -1.5/17, -1.5/17, -4.5/17, 1.5/17, 1.5/17},
{-4.5/17, -0.5/17, -0.5/17, 5.5/17, 0.5/17, 0.5/17},
{5.5/17, -1.5/17, -1.5/17, 6.5/17, 1.5/17, 1.5/17},
-- Tip
{-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},
-- Fletching
{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},
}
},
tiles = arrow_image(color, 100),
use_texture_alpha = minetest.features.use_texture_alpha_string_modes and "opaque" or false,
paramtype = "light",
paramtype2 = "facedir",
sunlight_propagates = true,
groups = {not_in_creative_inventory=1, dig_immediate=3},
drop = "",
node_placement_prediction = "",
on_construct = function(pos)
minetest.log("error", "[mcl_potions] Trying to construct mcl_potions:"..name.."arrow_box at "..minetest.pos_to_string(pos))
minetest.remove_node(pos)
end,
})
local ARROW_ENTITY={
physical = true,
visual = "mesh",
mesh = "mcl_bows_arrow.obj",
visual_size = {x=-1, y=1},
textures = arrow_image(color, 100),
collisionbox = {-0.19, -0.125, -0.19, 0.19, 0.125, 0.19},
collide_with_objects = false,
_lastpos={},
_startpos=nil,
_damage=1, -- Damage on impact
_stuck=false, -- Whether arrow is stuck
_stucktimer=nil,-- Amount of time (in seconds) the arrow has been stuck so far
_stuckrechecktimer=nil,-- An additional timer for periodically re-checking the stuck status of an arrow
_stuckin=nil, --Position of node in which arow is stuck.
_shooter=nil, -- ObjectRef of player or mob who shot it
_viscosity=0, -- Viscosity of node the arrow is currently in
_deflection_cooloff=0, -- Cooloff timer after an arrow deflection, to prevent many deflections in quick succession
}
-- Destroy arrow entity self at pos and drops it as an item
local function spawn_item(self, pos)
if not minetest.is_creative_enabled("") then
local item = minetest.add_item(pos, "mcl_potions:"..name.."_arrow")
item:set_velocity({x=0, y=0, z=0})
item:set_yaw(self.object:get_yaw())
end
self.object:remove()
end
function ARROW_ENTITY.on_step(self, dtime)
local pos = self.object:get_pos()
local dpos = table.copy(pos) -- digital pos
dpos = vector.round(dpos)
local node = minetest.get_node(dpos)
if self._stuck then
self._stucktimer = self._stucktimer + dtime
self._stuckrechecktimer = self._stuckrechecktimer + dtime
if self._stucktimer > ARROW_TIMEOUT then
self.object:remove()
return
end
-- Drop arrow as item when it is no longer stuck
-- FIXME: Arrows are a bit slow to react and continue to float in mid air for a few seconds.
if self._stuckrechecktimer > STUCK_RECHECK_TIME then
local stuckin_def
if self._stuckin then
stuckin_def = minetest.registered_nodes[minetest.get_node(self._stuckin).name]
end
-- TODO: In MC, arrow just falls down without turning into an item
if stuckin_def and stuckin_def.walkable == false then
spawn_item(self, pos)
return
end
self._stuckrechecktimer = 0
end
-- Pickup arrow if player is nearby (not in Creative Mode)
local objects = minetest.get_objects_inside_radius(pos, 1)
for _,obj in ipairs(objects) do
if obj:is_player() then
if not minetest.is_creative_enabled(obj:get_player_name()) then
if obj:get_inventory():room_for_item("main", "mcl_potions:"..name.."_arrow") then
obj:get_inventory():add_item("main", "mcl_potions:"..name.."_arrow")
minetest.sound_play("item_drop_pickup", {
pos = pos,
max_hear_distance = 16,
gain = 1.0,
}, true)
end
end
self.object:remove()
return
end
end
-- Check for object "collision". Done every tick (hopefully this is not too stressing)
else
if self._damage == 10 or self._damage == 9 then
minetest.add_particlespawner({
amount = 1,
time = .001,
minpos = pos,
maxpos = pos,
minvel = vector.new(-0.1,-0.1,-0.1),
maxvel = vector.new(0.1,0.1,0.1),
minexptime = 0.5,
maxexptime = 0.5,
minsize = 2,
maxsize = 2,
collisiondetection = false,
vertical = false,
texture = "mobs_mc_arrow_particle.png",
glow = 1,
})
end
-- We just check for any hurtable objects nearby.
-- The radius of 3 is fairly liberal, but anything lower than than will cause
-- arrow to hilariously go through mobs often.
-- TODO: Implement an ACTUAL collision detection (engine support needed).
local objs = minetest.get_objects_inside_radius(pos, 1.5)
local closest_object
local closest_distance
if self._deflection_cooloff > 0 then
self._deflection_cooloff = self._deflection_cooloff - dtime
end
-- Iterate through all objects and remember the closest attackable object
local arrow_dir = self.object:get_velocity()
--create a raycast from the arrow based on the velocity of the arrow to deal with lag
local raycast = minetest.raycast(pos, vector.add(pos, vector.multiply(arrow_dir, 0.1)), true, false)
for hitpoint in raycast do
if hitpoint.type == "object" then
-- find the closest object that is in the way of the arrow
local ok = false
if hitpoint.ref:is_player() and enable_pvp then
ok = true
elseif not hitpoint.ref:is_player() and hitpoint.ref:get_luaentity() then
if (hitpoint.ref:get_luaentity().is_mob or hitpoint.ref:get_luaentity()._hittable_by_projectile) then
ok = true
end
end
if ok then
local dist = vector.distance(hitpoint.ref:get_pos(), pos)
if not closest_object or not closest_distance then
closest_object = hitpoint.ref
closest_distance = dist
elseif dist < closest_distance then
closest_object = hitpoint.ref
closest_distance = dist
end
end
end
end
-- If an attackable object was found, we will damage the closest one only
if closest_object then
local obj = closest_object
local is_player = obj:is_player()
local lua = obj:get_luaentity()
if obj ~= self._shooter and (is_player or (lua and lua.is_mob)) then
if obj:get_hp() > 0 then
-- Check if there is no solid node between arrow and object
local ray = minetest.raycast(self.object:get_pos(), obj:get_pos(), true)
for pointed_thing in ray do
if pointed_thing.type == "object" and pointed_thing.ref == closest_object then
-- Target reached! We can proceed now.
break
elseif pointed_thing.type == "node" then
local nn = minetest.get_node(minetest.get_pointed_thing_position(pointed_thing)).name
local nodedef = minetest.registered_nodes[nn]
if (not nodedef) or nodedef.walkable then
-- There's a node in the way. Delete arrow without damage
self.object:remove()
return
end
end
end
local potency = self._potency or 0 local potency = self._potency or 0
local plus = self._plus or 0 local plus = self._plus or 0
-- Punch target object but avoid hurting enderman.
if lua then
if lua.name ~= "mobs_mc:rover" then
obj:punch(self.object, 1.0, {
full_punch_interval=1.0,
damage_groups={fleshy=self._damage},
}, nil)
if def._effect_list then if def._effect_list then
local ef_level
local dur
for name, details in pairs(def._effect_list) do for name, details in pairs(def._effect_list) do
local ef_level = details.level
if details.uses_level then if details.uses_level then
ef_level = details.level + details.level_scaling * (potency) ef_level = details.level + details.level_scaling * potency
else
ef_level = details.level
end end
local dur = details.dur
if details.dur_variable then if details.dur_variable then
dur = details.dur * math.pow(mcl_potions.PLUS_FACTOR, plus) dur = details.dur * math.pow(mcl_potions.PLUS_FACTOR, plus)
if potency>0 and details.uses_level then if potency>0 and details.uses_level then
dur = dur / math.pow(mcl_potions.POTENT_FACTOR, potency) dur = dur / math.pow(mcl_potions.POTENT_FACTOR, potency)
end end
else
dur = details.dur
end end
dur = dur * mcl_potions.SPLASH_FACTOR dur = dur * mcl_potions.SPLASH_FACTOR
if details.effect_stacks then if details.effect_stacks then
ef_level = ef_level + mcl_potions.get_effect_level(obj, name) ef_level = ef_level + mcl_potions.get_effect_level(obj, name)
end end
@ -309,218 +72,16 @@ function mcl_potions.register_arrow(name, desc, color, def)
end end
end end
if def.custom_effect then def.custom_effect(obj, potency+1, plus) end if def.custom_effect then def.custom_effect(obj, potency+1, plus) end
end end,
else })
obj:punch(self.object, 1.0, {
full_punch_interval=1.0,
damage_groups={fleshy=self._damage},
}, nil)
if def._effect_list then
local ef_level
local dur
for name, details in pairs(def._effect_list) do
if details.uses_level then
ef_level = details.level + details.level_scaling * (potency)
else
ef_level = details.level
end
if details.dur_variable then
dur = details.dur * math.pow(mcl_potions.PLUS_FACTOR, plus)
if potency>0 and details.uses_level then
dur = dur / math.pow(mcl_potions.POTENT_FACTOR, potency)
end
else
dur = details.dur
end
dur = dur * mcl_potions.SPLASH_FACTOR
if details.effect_stacks then
ef_level = ef_level + mcl_potions.get_effect_level(obj, name)
end
mcl_potions.give_effect_by_level(name, obj, ef_level, dur)
end
end
if def.custom_effect then def.custom_effect(obj, potency+1, plus) end
end
if is_player then -- Entity for older-style arrows
if self._shooter and self._shooter:is_player() then local arrow_entity = table.copy(mcl_bows.arrow_entity)
-- “Ding” sound for hitting another player arrow_entity.on_activate = function(self, staticdata, dtime_s)
minetest.sound_play({name="mcl_bows_hit_player", gain=0.1}, {to_player=self._shooter:get_player_name()}, true) mcl_bows.arrow_entity.on_activate(self, staticdata, dtime_s)
self._arrow_item = arrow_item
end end
end minetest.register_entity("mcl_potions:"..name.."_arrow_entity", arrow_entity)
if lua then
local entity_name = lua.name
-- Achievement for hitting skeleton, wither skeleton or stray (TODO) with an arrow at least 50 meters away
-- NOTE: Range has been reduced because mobs unload much earlier than that ... >_>
-- TODO: This achievement should be given for the kill, not just a hit
if self._shooter and self._shooter:is_player() and vector.distance(pos, self._startpos) >= 20 then
if mod_awards and (entity_name == "mobs_mc:skeleton" or entity_name == "mobs_mc:stray" or entity_name == "mobs_mc:witherskeleton") then
awards.unlock(self._shooter:get_player_name(), "mcl:snipeSkeleton")
end
end
end
end
self.object:remove()
return
end
end
end
-- Check for node collision
if self._lastpos.x~=nil and not self._stuck then
local nodedef = minetest.registered_nodes[node.name]
local vel = self.object:get_velocity()
-- Arrow has stopped in one axis, so it probably hit something.
-- This detection is a bit clunky, but sadly, MT does not offer a direct collision detection for us. :-(
if (math.abs(vel.x) < 0.0001) or (math.abs(vel.z) < 0.0001) or (math.abs(vel.y) < 0.00001) then
-- Check for the node to which the arrow is pointing
local dir
if math.abs(vel.y) < 0.00001 then
if self._lastpos.y < pos.y then
dir = {x=0, y=1, z=0}
else
dir = {x=0, y=-1, z=0}
end
else
dir = minetest.facedir_to_dir(minetest.dir_to_facedir(minetest.yaw_to_dir(self.object:get_yaw()-YAW_OFFSET)))
end
self._stuckin = vector.add(dpos, dir)
local snode = minetest.get_node(self._stuckin)
local sdef = minetest.registered_nodes[snode.name]
-- If node is non-walkable, unknown or ignore, don't make arrow stuck.
-- This causes a deflection in the engine.
if not sdef or sdef.walkable == false or snode.name == "ignore" then
self._stuckin = nil
if self._deflection_cooloff <= 0 then
-- Lose 1/3 of velocity on deflection
local newvel = vector.multiply(vel, 0.6667)
self.object:set_velocity(newvel)
-- Reset deflection cooloff timer to prevent many deflections happening in quick succession
self._deflection_cooloff = 1.0
end
else
-- Node was walkable, make arrow stuck
self._stuck = true
self._stucktimer = 0
self._stuckrechecktimer = 0
self.object:set_velocity({x=0, y=0, z=0})
self.object:set_acceleration({x=0, y=0, z=0})
-- Activate target
if mod_target and snode.name == "mcl_target:target_off" then
mcl_target.hit(self._stuckin, 1) --10 redstone ticks
end
-- Push the button! Push, push, push the button!
if mod_button and minetest.get_item_group(node.name, "button") > 0 and minetest.get_item_group(node.name, "button_push_by_arrow") == 1 then
local bdir = minetest.wallmounted_to_dir(node.param2)
-- Check the button orientation
if vector.equals(vector.add(dpos, bdir), self._stuckin) then
mesecon.push_button(dpos, node)
end
end
end
elseif (nodedef and nodedef.liquidtype ~= "none") then
-- Slow down arrow in liquids
local v = nodedef.liquid_viscosity
if not v then
v = 0
end
--local old_v = self._viscosity
self._viscosity = v
local vpenalty = math.max(0.1, 0.98 - 0.1 * v)
if math.abs(vel.x) > 0.001 then
vel.x = vel.x * vpenalty
end
if math.abs(vel.z) > 0.001 then
vel.z = vel.z * vpenalty
end
self.object:set_velocity(vel)
end
end
-- Update yaw
if not self._stuck then
local vel = self.object:get_velocity()
local yaw = minetest.dir_to_yaw(vel)+YAW_OFFSET
local pitch = dir_to_pitch(vel)
self.object:set_rotation({ x = 0, y = yaw, z = pitch })
end
-- Update internal variable
self._lastpos={x=pos.x, y=pos.y, z=pos.z}
end
-- Force recheck of stuck arrows when punched.
-- Otherwise, punching has no effect.
function ARROW_ENTITY.on_punch(self)
if self._stuck then
self._stuckrechecktimer = STUCK_RECHECK_TIME
end
end
function ARROW_ENTITY.get_staticdata(self)
local out = {
lastpos = self._lastpos,
startpos = self._startpos,
damage = self._damage,
stuck = self._stuck,
stuckin = self._stuckin,
}
if self._stuck then
-- If _stucktimer is missing for some reason, assume the maximum
if not self._stucktimer then
self._stucktimer = ARROW_TIMEOUT
end
out.stuckstarttime = minetest.get_gametime() - self._stucktimer
end
if self._shooter and self._shooter:is_player() then
out.shootername = self._shooter:get_player_name()
end
return minetest.serialize(out)
end
function ARROW_ENTITY.on_activate(self, staticdata, dtime_s)
local data = minetest.deserialize(staticdata)
if data then
self._stuck = data.stuck
if data.stuck then
if data.stuckstarttime then
-- First, check if the stuck arrow is aleady past its life timer.
-- If yes, delete it.
self._stucktimer = minetest.get_gametime() - data.stuckstarttime
if self._stucktimer > ARROW_TIMEOUT then
self.object:remove()
return
end
end
-- Perform a stuck recheck on the next step.
self._stuckrechecktimer = STUCK_RECHECK_TIME
self._stuckin = data.stuckin
end
-- Get the remaining arrow state
self._lastpos = data.lastpos
self._startpos = data.startpos
self._damage = data.damage
if data.shootername then
local shooter = minetest.get_player_by_name(data.shootername)
if shooter and shooter:is_player() then
self._shooter = shooter
end
end
end
self.object:set_armor_groups({ immortal = 1 })
end
minetest.register_entity("mcl_potions:"..name.."_arrow_entity", ARROW_ENTITY)
if minetest.get_modpath("mcl_bows") then if minetest.get_modpath("mcl_bows") then
minetest.register_craft({ minetest.register_craft({
@ -531,7 +92,6 @@ function mcl_potions.register_arrow(name, desc, color, def)
{"mcl_bows:arrow","mcl_bows:arrow","mcl_bows:arrow"} {"mcl_bows:arrow","mcl_bows:arrow","mcl_bows:arrow"}
} }
}) })
end end
if minetest.get_modpath("doc_identifier") then if minetest.get_modpath("doc_identifier") then

View File

@ -0,0 +1,83 @@
local modname = minetest.get_current_modname()
local S = minetest.get_translator(modname)
local mod_target = minetest.get_modpath("mcl_target")
local how_to_throw = S("Use the punch key to throw.")
-- Egg
minetest.register_craftitem("mcl_throwing:egg", {
description = S("Egg"),
_tt_help = S("Throwable").."\n"..S("Chance to hatch chicks when broken"),
_doc_items_longdesc = S("Eggs can be thrown or launched from a dispenser and breaks on impact. There is a small chance that 1 or even 4 chicks will pop out of the egg."),
_doc_items_usagehelp = how_to_throw,
inventory_image = "mcl_throwing_egg.png",
stack_max = 64,
on_use = mcl_throwing.get_player_throw_function("mcl_throwing:egg_entity"),
_on_dispense = mcl_throwing.dispense_function,
groups = { craftitem = 1 },
})
local function egg_spawn_chicks(pos)
-- 1/8 chance to spawn a chick
if math.random(1,8) ~= 1 then return end
mcl_mobs.spawn_child(pos, "mobs_mc:chicken")
-- BONUS ROUND: 1/32 chance to spawn 3 additional chicks
if math.random(1,32) ~= 1 then return end
mcl_mobs.spawn_child(vector.offset(pos, 0.7, 0, 0 ), "mobs_mc:chicken")
mcl_mobs.spawn_child(vector.offset(pos, -0.7, 0, -0.7), "mobs_mc:chicken")
mcl_mobs.spawn_child(vector.offset(pos, -0.7, 0, 0.7), "mobs_mc:chicken")
end
vl_projectile.register("mcl_throwing:egg_entity",{
physical = false,
timer=0,
textures = {"mcl_throwing_egg.png"},
visual_size = {x=0.45, y=0.45},
collisionbox = {0,0,0,0,0,0},
pointable = false,
get_staticdata = mcl_throwing.get_staticdata,
on_activate = mcl_throwing.on_activate,
on_step = vl_projectile.update_projectile,
_lastpos={},
_thrower = nil,
_vl_projectile = {
behaviors = {
vl_projectile.collides_with_solids,
vl_projectile.collides_with_entities,
},
allow_punching = function(self, _, _, object)
if self._owner == mcl_util.get_entity_id(object) then
return self.timer > 1
end
local le = object:get_luaentity()
return le and (le.is_mob or le._hittable_by_projectile) or object:is_player()
end,
on_collide_with_solid = function(self, pos, node)
if mod_target and node.name == "mcl_target:target_off" then
mcl_target.hit(vector.round(pos), 0.4) --4 redstone ticks
end
local vel = self.object:get_velocity()
pos = vector.round(pos + vector.normalize(vel) * -0.35)
egg_spawn_chicks(pos)
end,
on_collide_with_entity = function(self, pos, obj)
local vel = self.object:get_velocity()
pos = vector.round(pos + vector.normalize(vel) * -0.35)
egg_spawn_chicks(pos)
end,
sounds = {
on_collision = {"mcl_throwing_egg_impact", {max_hear_distance=10, gain=0.5}, true}
},
},
})
mcl_throwing.register_throwable_object("mcl_throwing:egg", "mcl_throwing:egg_entity", 22)

View File

@ -0,0 +1,142 @@
local modname = minetest.get_current_modname()
local S = minetest.get_translator(modname)
local math = math
local vector = vector
local mod_target = minetest.get_modpath("mcl_target")
local how_to_throw = S("Use the punch key to throw.")
-- Ender Pearl
minetest.register_craftitem("mcl_throwing:ender_pearl", {
description = S("Ender Pearl"),
_tt_help = S("Throwable").."\n"..minetest.colorize(mcl_colors.YELLOW, S("Teleports you on impact for cost of 5 HP")),
_doc_items_longdesc = S("An ender pearl is an item which can be used for teleportation at the cost of health. It can be thrown and teleport the thrower to its impact location when it hits a solid block or a plant. Each teleportation hurts the user by 5 hit points."),
_doc_items_usagehelp = how_to_throw,
wield_image = "mcl_throwing_ender_pearl.png",
inventory_image = "mcl_throwing_ender_pearl.png",
stack_max = 16,
on_use = mcl_throwing.get_player_throw_function("mcl_throwing:ender_pearl_entity"),
groups = { transport = 1 },
})
function on_collide(self, pos, node)
if mod_target and node.name == "mcl_target:target_off" then
mcl_target.hit(vector.round(pos), 0.4) --4 redstone ticks
end
if node.name == "ignore" then
-- FIXME: This also means the player loses an ender pearl for throwing into unloaded areas
return
end
-- Make sure we have a reference to the player
local player = self._thrower and minetest.get_player_by_name(self._thrower)
if not player then return end
-- Teleport and hurt player
-- First determine good teleport position
local dir = vector.zero()
local v = self.object:get_velocity()
local node_def = minetest.registered_nodes[node.name]
if node_def and node_def.walkable then
local vc = vector.normalize(v) -- vector for calculating
-- Node is walkable, we have to find a place somewhere outside of that node
-- Zero-out the two axes with a lower absolute value than the axis with the strongest force
local lv, ld = math.abs(vc.y), "y"
if math.abs(vc.x) > lv then
lv, ld = math.abs(vc.x), "x"
end
if math.abs(vc.z) > lv then
ld = "z" --math.abs(vc.z)
end
if ld ~= "x" then vc.x = 0 end
if ld ~= "y" then vc.y = 0 end
if ld ~= "z" then vc.z = 0 end
-- Final tweaks to the teleporting pos, based on direction
-- Impact from the side
dir.x = vc.x * -1
dir.z = vc.z * -1
-- Special case: top or bottom of node
if vc.y > 0 then
-- We need more space when impact is from below
dir.y = -2.3
elseif vc.y < 0 then
-- Standing on top
dir.y = 0.5
end
end
-- If node was not walkable, no modification to pos is made.
-- Final teleportation position
local telepos = vector.add(pos, dir)
local telenode = minetest.get_node(telepos)
--[[ It may be possible that telepos is walkable due to the algorithm.
Especially when the ender pearl is faster horizontally than vertical.
This applies final fixing, just to be sure we're not in a walkable node ]]
if not minetest.registered_nodes[telenode.name] or minetest.registered_nodes[telenode.name].walkable then
if v.y < 0 then
telepos.y = telepos.y + 0.5
else
telepos.y = telepos.y - 2.3
end
end
local oldpos = player:get_pos()
-- Teleport and hurt player
player:set_pos(telepos)
player:set_hp(player:get_hp() - 5, { type = "fall", from = "mod" })
-- 5% chance to spawn endermite at the player's origin
if math.random(1,20) == 1 then
minetest.add_entity(oldpos, "mobs_mc:endermite")
end
end
-- Ender pearl entity
vl_projectile.register("mcl_throwing:ender_pearl_entity",{
physical = false,
timer=0,
textures = {"mcl_throwing_ender_pearl.png"},
visual_size = {x=0.9, y=0.9},
collisionbox = {0,0,0,0,0,0},
pointable = false,
get_staticdata = mcl_throwing.get_staticdata,
on_activate = mcl_throwing.on_activate,
on_step = vl_projectile.update_projectile,
_lastpos={},
_thrower = nil, -- Player ObjectRef of the player who threw the ender pearl
_vl_projectile = {
behaviors = {
vl_projectile.collides_with_solids,
vl_projectile.collides_with_entities,
},
collides_with = {
"mcl_core:vine", "mcl_core:deadbush",
"group:flower", "group:sapling",
"group:plant", "group:mushroom",
},
allow_punching = function(self, _, _, object)
if self._owner == mcl_util.get_entity_id(object) then
return self.timer > 1
end
local le = object:get_luaentity()
return le and (le.is_mob or le._hittable_by_projectile) or object:is_player()
end,
on_collide_with_entity = function(self, pos, entity)
on_collide(self, pos, minetest.get_node(pos))
end,
on_collide_with_solid = on_collide,
},
})
mcl_throwing.register_throwable_object("mcl_throwing:ender_pearl", "mcl_throwing:ender_pearl_entity", 22)

View File

@ -6,32 +6,29 @@ local modpath = minetest.get_modpath(minetest.get_current_modname())
-- Snowballs and other throwable items -- Snowballs and other throwable items
-- --
local GRAVITY = tonumber(minetest.settings:get("movement_gravity"))
local entity_mapping = {} local entity_mapping = {}
local velocities = {} local velocities = {}
function mcl_throwing.register_throwable_object(name, entity, velocity) function mcl_throwing.register_throwable_object(name, entity, velocity)
entity_mapping[name] = entity entity_mapping[name] = entity
velocities[name] = velocity velocities[name] = velocity
assert(minetest.registered_entities[entity], entity.." not registered")
assert(minetest.registered_entities[entity]._vl_projectile)
end end
function mcl_throwing.throw(throw_item, pos, dir, velocity, thrower) function mcl_throwing.throw(throw_item, pos, dir, velocity, thrower)
if velocity == nil then velocity = velocity or velocities[throw_item] or 22
velocity = velocities[throw_item]
end
if velocity == nil then
velocity = 22
end
minetest.sound_play("mcl_throwing_throw", {pos=pos, gain=0.4, max_hear_distance=16}, true) minetest.sound_play("mcl_throwing_throw", {pos=pos, gain=0.4, max_hear_distance=16}, true)
local itemstring = ItemStack(throw_item):get_name() local itemstring = ItemStack(throw_item):get_name()
local obj = minetest.add_entity(pos, entity_mapping[itemstring]) local obj = vl_projectile.create(entity_mapping[itemstring], {
obj:set_velocity({x=dir.x*velocity, y=dir.y*velocity, z=dir.z*velocity}) pos = pos,
obj:set_acceleration({x=dir.x*-3, y=-GRAVITY, z=dir.z*-3}) owner_id = thrower,
if thrower then dir = dir,
velocity = velocity,
drag = 3,
})
obj:get_luaentity()._thrower = thrower obj:get_luaentity()._thrower = thrower
end
return obj return obj
end end
@ -71,6 +68,7 @@ end
function mcl_throwing.on_activate(self, staticdata, dtime_s) function mcl_throwing.on_activate(self, staticdata, dtime_s)
local data = minetest.deserialize(staticdata) local data = minetest.deserialize(staticdata)
self._staticdata = data
if data then if data then
self._lastpos = data._lastpos self._lastpos = data._lastpos
self._thrower = data._thrower self._thrower = data._thrower

View File

@ -1,3 +1,3 @@
name = mcl_throwing name = mcl_throwing
depends = mcl_colors depends = mcl_colors, vl_projectile
optional_depends = mcl_core, mcl_mobitems, doc, mcl_target optional_depends = mcl_core, mcl_mobitems, doc, mcl_target

View File

@ -1,322 +1,7 @@
local S = minetest.get_translator(minetest.get_current_modname()) local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
local math = math dofile(modpath.."/snowball.lua")
local vector = vector dofile(modpath.."/egg.lua")
dofile(modpath.."/ender_pearl.lua")
local mod_target = minetest.get_modpath("mcl_target")
-- The snowball entity
local snowball_ENTITY={
physical = false,
timer=0,
textures = {"mcl_throwing_snowball.png"},
visual_size = {x=0.5, y=0.5},
collisionbox = {0,0,0,0,0,0},
pointable = false,
get_staticdata = mcl_throwing.get_staticdata,
on_activate = mcl_throwing.on_activate,
_thrower = nil,
_lastpos={},
}
local egg_ENTITY={
physical = false,
timer=0,
textures = {"mcl_throwing_egg.png"},
visual_size = {x=0.45, y=0.45},
collisionbox = {0,0,0,0,0,0},
pointable = false,
get_staticdata = mcl_throwing.get_staticdata,
on_activate = mcl_throwing.on_activate,
_thrower = nil,
_lastpos={},
}
-- Ender pearl entity
local pearl_ENTITY={
physical = false,
timer=0,
textures = {"mcl_throwing_ender_pearl.png"},
visual_size = {x=0.9, y=0.9},
collisionbox = {0,0,0,0,0,0},
pointable = false,
get_staticdata = mcl_throwing.get_staticdata,
on_activate = mcl_throwing.on_activate,
_lastpos={},
_thrower = nil, -- Player ObjectRef of the player who threw the ender pearl
}
local function check_object_hit(self, pos, dmg)
for _,object in pairs(minetest.get_objects_inside_radius(pos, 1.5)) do
local entity = object:get_luaentity()
if entity
and entity.name ~= self.object:get_luaentity().name then
if object:is_player() and self._thrower ~= object:get_player_name() then
self.object:remove()
return true
elseif (entity.is_mob == true or entity._hittable_by_projectile) and (self._thrower ~= object) then
object:punch(self.object, 1.0, {
full_punch_interval = 1.0,
damage_groups = dmg,
}, nil)
return true
end
end
end
return false
end
local function snowball_particles(pos, vel)
local vel = vector.normalize(vector.multiply(vel, -1))
minetest.add_particlespawner({
amount = 20,
time = 0.001,
minpos = pos,
maxpos = pos,
minvel = vector.add({x=-2, y=3, z=-2}, vel),
maxvel = vector.add({x=2, y=5, z=2}, vel),
minacc = {x=0, y=-9.81, z=0},
maxacc = {x=0, y=-9.81, z=0},
minexptime = 1,
maxexptime = 3,
minsize = 0.7,
maxsize = 0.7,
collisiondetection = true,
collision_removal = true,
object_collision = false,
texture = "weather_pack_snow_snowflake"..math.random(1,2)..".png",
})
end
-- Snowball on_step()--> called when snowball is moving.
local function snowball_on_step(self, dtime)
self.timer = self.timer + dtime
local pos = self.object:get_pos()
local vel = self.object:get_velocity()
local node = minetest.get_node(pos)
local def = minetest.registered_nodes[node.name]
-- Destroy when hitting a solid node
if self._lastpos.x~=nil then
if (def and def.walkable) or not def then
minetest.sound_play("mcl_throwing_snowball_impact_hard", { pos = pos, max_hear_distance=16, gain=0.7 }, true)
snowball_particles(self._lastpos, vel)
self.object:remove()
if mod_target and node.name == "mcl_target:target_off" then
mcl_target.hit(vector.round(pos), 0.4) --4 redstone ticks
end
return
end
end
if check_object_hit(self, pos, {snowball_vulnerable = 3}) then
minetest.sound_play("mcl_throwing_snowball_impact_soft", { pos = pos, max_hear_distance=16, gain=0.7 }, true)
snowball_particles(pos, vel)
self.object:remove()
return
end
self._lastpos = pos -- Set _lastpos-->Node will be added at last pos outside the node
end
-- Movement function of egg
local function egg_on_step(self, dtime)
self.timer = self.timer + dtime
local pos = self.object:get_pos()
local node = minetest.get_node(pos)
local def = minetest.registered_nodes[node.name]
-- Destroy when hitting a solid node or entity, with chance to spawn chicks
if (def and def.walkable) or not def or check_object_hit(self, pos, 0) then
-- If egg has just been thrown, use current position
if not self._lastpos.x then
self._lastpos = pos
end
-- 1/8 chance to spawn a chick
-- FIXME: Chicks have a quite good chance to spawn in walls
if math.random(1,8) == 1 then
mcl_mobs.spawn_child(self._lastpos, "mobs_mc:chicken")
-- BONUS ROUND: 1/32 chance to spawn 3 additional chicks
if math.random(1,32) == 1 then
local offsets = {
{ x=0.7, y=0, z=0 },
{ x=-0.7, y=0, z=-0.7 },
{ x=-0.7, y=0, z=0.7 },
}
for o=1, 3 do
local pos = vector.add(self._lastpos, offsets[o])
mcl_mobs.spawn_child(pos, "mobs_mc:chicken")
end
end
end
minetest.sound_play("mcl_throwing_egg_impact", { pos = self.object:get_pos(), max_hear_distance=10, gain=0.5 }, true)
self.object:remove()
if mod_target and node.name == "mcl_target:target_off" then
mcl_target.hit(vector.round(pos), 0.4) --4 redstone ticks
end
return
end
self._lastpos = pos -- Set lastpos-->Node will be added at last pos outside the node
end
-- Movement function of ender pearl
local function pearl_on_step(self, dtime)
self.timer = self.timer + dtime
local pos = self.object:get_pos()
pos.y = math.floor(pos.y)
local node = minetest.get_node(pos)
local nn = node.name
local def = minetest.registered_nodes[node.name]
-- Destroy when hitting a solid node
if self._lastpos.x~=nil then
local walkable = (def and def.walkable)
-- No teleport for hitting ignore for now. Otherwise the player could get stuck.
-- FIXME: This also means the player loses an ender pearl for throwing into unloaded areas
if node.name == "ignore" then
self.object:remove()
-- Activate when hitting a solid node or a plant
elseif walkable or nn == "mcl_core:vine" or nn == "mcl_core:deadbush" or minetest.get_item_group(nn, "flower") ~= 0 or minetest.get_item_group(nn, "sapling") ~= 0 or minetest.get_item_group(nn, "plant") ~= 0 or minetest.get_item_group(nn, "mushroom") ~= 0 or not def then
local player = self._thrower and minetest.get_player_by_name(self._thrower)
if player then
-- Teleport and hurt player
-- First determine good teleport position
local dir = {x=0, y=0, z=0}
local v = self.object:get_velocity()
if walkable then
local vc = table.copy(v) -- vector for calculating
-- Node is walkable, we have to find a place somewhere outside of that node
vc = vector.normalize(vc)
-- Zero-out the two axes with a lower absolute value than
-- the axis with the strongest force
local lv, ld
lv, ld = math.abs(vc.y), "y"
if math.abs(vc.x) > lv then
lv, ld = math.abs(vc.x), "x"
end
if math.abs(vc.z) > lv then
ld = "z" --math.abs(vc.z)
end
if ld ~= "x" then vc.x = 0 end
if ld ~= "y" then vc.y = 0 end
if ld ~= "z" then vc.z = 0 end
-- Final tweaks to the teleporting pos, based on direction
-- Impact from the side
dir.x = vc.x * -1
dir.z = vc.z * -1
-- Special case: top or bottom of node
if vc.y > 0 then
-- We need more space when impact is from below
dir.y = -2.3
elseif vc.y < 0 then
-- Standing on top
dir.y = 0.5
end
end
-- If node was not walkable, no modification to pos is made.
-- Final teleportation position
local telepos = vector.add(pos, dir)
local telenode = minetest.get_node(telepos)
--[[ It may be possible that telepos is walkable due to the algorithm.
Especially when the ender pearl is faster horizontally than vertical.
This applies final fixing, just to be sure we're not in a walkable node ]]
if not minetest.registered_nodes[telenode.name] or minetest.registered_nodes[telenode.name].walkable then
if v.y < 0 then
telepos.y = telepos.y + 0.5
else
telepos.y = telepos.y - 2.3
end
end
local oldpos = player:get_pos()
-- Teleport and hurt player
player:set_pos(telepos)
player:set_hp(player:get_hp() - 5, { type = "fall", from = "mod" })
-- 5% chance to spawn endermite at the player's origin
local r = math.random(1,20)
if r == 1 then
minetest.add_entity(oldpos, "mobs_mc:endermite")
end
end
self.object:remove()
if mod_target and node.name == "mcl_target:target_off" then
mcl_target.hit(vector.round(pos), 0.4) --4 redstone ticks
end
return
end
end
self._lastpos = pos -- Set lastpos-->Node will be added at last pos outside the node
end
snowball_ENTITY.on_step = snowball_on_step
egg_ENTITY.on_step = egg_on_step
pearl_ENTITY.on_step = pearl_on_step
minetest.register_entity("mcl_throwing:snowball_entity", snowball_ENTITY)
minetest.register_entity("mcl_throwing:egg_entity", egg_ENTITY)
minetest.register_entity("mcl_throwing:ender_pearl_entity", pearl_ENTITY)
local how_to_throw = S("Use the punch key to throw.")
-- Snowball
minetest.register_craftitem("mcl_throwing:snowball", {
description = S("Snowball"),
_tt_help = S("Throwable"),
_doc_items_longdesc = S("Snowballs can be thrown or launched from a dispenser for fun. Hitting something with a snowball does nothing."),
_doc_items_usagehelp = how_to_throw,
inventory_image = "mcl_throwing_snowball.png",
stack_max = 64,
groups = { weapon_ranged = 1 },
on_use = mcl_throwing.get_player_throw_function("mcl_throwing:snowball_entity"),
_on_dispense = mcl_throwing.dispense_function,
})
-- Egg
minetest.register_craftitem("mcl_throwing:egg", {
description = S("Egg"),
_tt_help = S("Throwable").."\n"..S("Chance to hatch chicks when broken"),
_doc_items_longdesc = S("Eggs can be thrown or launched from a dispenser and breaks on impact. There is a small chance that 1 or even 4 chicks will pop out of the egg."),
_doc_items_usagehelp = how_to_throw,
inventory_image = "mcl_throwing_egg.png",
stack_max = 64,
on_use = mcl_throwing.get_player_throw_function("mcl_throwing:egg_entity"),
_on_dispense = mcl_throwing.dispense_function,
groups = { craftitem = 1 },
})
-- Ender Pearl
minetest.register_craftitem("mcl_throwing:ender_pearl", {
description = S("Ender Pearl"),
_tt_help = S("Throwable").."\n"..minetest.colorize(mcl_colors.YELLOW, S("Teleports you on impact for cost of 5 HP")),
_doc_items_longdesc = S("An ender pearl is an item which can be used for teleportation at the cost of health. It can be thrown and teleport the thrower to its impact location when it hits a solid block or a plant. Each teleportation hurts the user by 5 hit points."),
_doc_items_usagehelp = how_to_throw,
wield_image = "mcl_throwing_ender_pearl.png",
inventory_image = "mcl_throwing_ender_pearl.png",
stack_max = 16,
on_use = mcl_throwing.get_player_throw_function("mcl_throwing:ender_pearl_entity"),
groups = { transport = 1 },
})
mcl_throwing.register_throwable_object("mcl_throwing:snowball", "mcl_throwing:snowball_entity", 22)
mcl_throwing.register_throwable_object("mcl_throwing:egg", "mcl_throwing:egg_entity", 22)
mcl_throwing.register_throwable_object("mcl_throwing:ender_pearl", "mcl_throwing:ender_pearl_entity", 22)

View File

@ -0,0 +1,83 @@
local modname = minetest.get_current_modname()
local S = minetest.get_translator(modname)
local mod_target = minetest.get_modpath("mcl_target")
local how_to_throw = S("Use the punch key to throw.")
-- Snowball
minetest.register_craftitem("mcl_throwing:snowball", {
description = S("Snowball"),
_tt_help = S("Throwable"),
_doc_items_longdesc = S("Snowballs can be thrown or launched from a dispenser for fun. Hitting something with a snowball does nothing."),
_doc_items_usagehelp = how_to_throw,
inventory_image = "mcl_throwing_snowball.png",
stack_max = 64,
groups = { weapon_ranged = 1 },
on_use = mcl_throwing.get_player_throw_function("mcl_throwing:snowball_entity"),
_on_dispense = mcl_throwing.dispense_function,
})
-- The snowball entity
local function snowball_particles(pos, vel)
local vel = vector.normalize(vector.multiply(vel, -1))
minetest.add_particlespawner({
amount = 20,
time = 0.001,
minpos = pos,
maxpos = pos,
minvel = vector.offset(vel, -2, 3, -2),
maxvel = vector.offset(vel, 2, 5, 2),
minacc = {x=0, y=-9.81, z=0},
maxacc = {x=0, y=-9.81, z=0},
minexptime = 1,
maxexptime = 3,
minsize = 0.7,
maxsize = 0.7,
collisiondetection = true,
collision_removal = true,
object_collision = false,
texture = "weather_pack_snow_snowflake"..math.random(1,2)..".png",
})
end
vl_projectile.register("mcl_throwing:snowball_entity", {
physical = false,
timer=0,
textures = {"mcl_throwing_snowball.png"},
visual_size = {x=0.5, y=0.5},
collisionbox = {0,0,0,0,0,0},
pointable = false,
get_staticdata = mcl_throwing.get_staticdata,
on_activate = mcl_throwing.on_activate,
_vl_projectile = {
behaviors = {
vl_projectile.collides_with_solids,
vl_projectile.collides_with_entities,
},
allow_punching = function(self, _, _, object)
if self._owner == mcl_util.get_entity_id(object) then
return self.timer > 1
end
local le = object:get_luaentity()
return le and (le.is_mob or le._hittable_by_projectile) or object:is_player()
end,
on_collide_with_solid = function(self, pos, node)
if mod_target and node.name == "mcl_target:target_off" then
mcl_target.hit(vector.round(pos), 0.4) --4 redstone ticks
end
snowball_particles(self._last_pos or pos, self.object:get_velocity())
end,
on_collide_with_entity = function(self, pos, entity)
snowball_particles(self._last_pos or pos, self.object:get_velocity())
end,
sounds = {
on_solid_collision = {"mcl_throwing_snowball_impact_hard", { max_hear_distance=16, gain=0.7 }, true},
on_entity_collision = {"mcl_throwing_snowball_impact_soft", { max_hear_distance=16, gain=0.7 }, true}
},
damage_groups = { snowball_vulnerable = 3 },
},
})
mcl_throwing.register_throwable_object("mcl_throwing:snowball", "mcl_throwing:snowball_entity", 22)

View File

@ -110,6 +110,13 @@ minetest.register_node("mcl_tnt:tnt", {
tnt.ignite(droppos) tnt.ignite(droppos)
end end
end, end,
_vl_projectile = {
on_collide = function(projectile, pos, node, node_def)
if mcl_burning.is_burning(projectile) then
tnt.ignite(pos)
end
end
},
sounds = sounds, sounds = sounds,
}) })

View File

@ -0,0 +1,100 @@
# Projectiles API
## `vl_projectile.register(entity_name, def)`
Registers a projectile entity.
Arguments:
* `entity_name`: The name the entity will be refered to by the minetest engine
* `def`: Projectile defintion. Supports all fields that standard minetest entities support.
Must include the field `_vl_projectile` for projectile-specific behaviors. These are the supported
fields:
* `ignore_gravity`: if true, the projectile will not be affected by gravity
* `liquid_drag`: if true, apply drag from liquid nodes to the projectile
* `survive_collision`: if this field is `false` or `nil`, the projectile will be removed after a collision.
* `sticks_in_players`: if true, the projectile will stick into players after colliding with them.
* `damages_players`: if true, the projectile will deal damage to players.
* `damage_groups`: damage group information to use for `punch()`. May be a function of type `function(projectile, entity_def, projectile_def, obj)`
that returns dynamic damange group information.
* `allow_punching`: will the projectile punch entities it collides with. May be either a boolean or a function of type `function(projectile, entity_def, projectile_def, obj)`.
* `survive_collision`: will the projectile surive collisions. May be either a boolean or a fnction of type `function(projectile, entity_def, projectile_def, type, ...)`.
* If `type` is "node" then the additional parameters `node, node_def` will be provided.
* If `type` is "entity" then the additional parameter `objet` will be provided.
* `behaviors`: a list of behavior callbacks that define the projectile's behavior. This mod provides the following
behaviors: `vl_projectiles.collides_with_solids`, `vl_projectiles.collides_with_entities` and `vl_projectiles.raycast_collides_with_entities`
* `maximum_time`: number of seconds until projectiles are removed.
* `sounds`: sounds for this projectile. All fields take a table with three parameters corresponding to the
three parameters for `minetest.play_sound()`. Supported sounds are:
* `on_collision`: played when no other more specific sound is defined. May be a function of type `function(projectile, entity_def, projectile_def, type, ...)`
* `on_solid_collision`: played when the projectile collides with a solid node. May be a function of type
`funciton(projectile, entity_def, projectile_def, type, pos, node, node_def)` with `type = "node"`
* `on_entity_collision`: played when the projectile collides with another entity. May be a function of type
`function(projectile, entity_def, projectile_def, type, entity)` with `type = "entity"`
* `on_collide_with_solid`: callback of type `function(projectile, pos, node, node_def)` used when the projectile collides with a solid node. Requires
`vl_projectile.collides_with_solids` in `behaviors` list.
* `on_collide_with_entity`: callback of type `function(projectile, pos, obj)` used when the projectile collides with an entity. Requires
`vl_projectile.collides_with_entities` in `behaviors` list.
## `vl_projectile.update_projectile(self, dtime)`
Performs standard projectile update logic and runs projectile behaviors.
Arguments:
* `self`: The lua entity of the projectile to update
* `dtime`: The amount of time that has passed since the last update. Nomally the `dtime`
parameter of the entity's `on_step(self, dtime)` callback.
## `vl_projectile.create(entity_id, options)`
Creates a projectile and performs convenience initialization.
Arguments:
* `entity_id`: The name the entity as passed to `vl_projectile.register()`
* `options`: A table with optional parameters. Supported fields are:
* `dir`: direction the projectile is moving in
* `velocity`: scalar velocity amount
* `drag`: scalar resistance to velocity
* `owner`: passed thru unmodified
* `extra`: passed thru unmodified
## `vl_projectile.replace_with_item_drop(projectile_lua_entity, pos, projectile_def)`
Removes the projectile and replaces it with an item entity based on either the entity's `_arrow_item` field or
the value `self._vl_projectile.item`.
Arguments:
* `projectile_lua_entity`: the lua entity of the projectile to be replaced.
* `pos`: the position to create the item entity
* `projectile_def`: The projectile's `_vl_projectile` field. If not provided, it will be
extracted from the projectile's lua entity.
## Custom Projectile Behaviors
The projectile API supports specifying the behaviors that a projectile will exhibit. There are several
standard behaviors provided with the API:
* `vl_projectile.burns`: projectile can be set on fire
* `vl_projectile.collides_with_solids`: handles collisions between projectiles and solid nodes
* `vl_projectile.collides_with_entities`: handles collisions between projectiles and entities by checking nearby entities
* `vl_projectile.has_tracer`: projectile will have a tracer trail when thrown/shot. Projectile can define
`_vl_projectile.hide_tracer = function(self)` to conditionally hide the tracer.
* `vl_projectile.sticks`: projectile will stick into nodes. Forces `_vl_projectile.sticks_in_nodes = true`
and `_vl_projectile.survive_collision = true`.
* `vl_projectile.raycast_collides_with_entities`: handles collisions between projectils and entities by performing a raycast
check along the path of movement.
Custom behaviors can be provided by adding a function with the signature `function(self, dtime, entity_def, projectile_def)`
to the list of behaviors a projectile supports.
Arguments:
* `self`: The lua entity of the projectile
* `dtime`: The amount of time that has passed since the last update. Nomally the `dtime`
parameter of the entity's `on_step(self, dtime)` callback.
* `entity_def`: The definition from `minetest.registered_entities` for the projectile.
* `projectile_def`: Same as `entity_def._vl_projectile`

View File

@ -0,0 +1,626 @@
vl_projectile = {}
local mod = vl_projectile
local vl_physics_path = minetest.get_modpath("vl_physics")
local DEBUG = false
local YAW_OFFSET = -math.pi/2
local GRAVITY = tonumber(minetest.settings:get("movement_gravity"))
local STUCK_TIMEOUT = 60
local STUCK_RECHECK_TIME = 0.25
local enable_pvp = minetest.settings:get_bool("enable_pvp")
function mod.projectile_physics(obj, entity_def, v, a)
local le = obj:get_luaentity()
if not le then return end
local entity_def = minetest.registered_entities[le.name]
local pos = obj:get_pos()
if not pos then return end
if vl_physics_path then
v,a = vl_physics.apply_entity_environmental_physics(obj)
else
-- Simple physics
v = v or obj:get_velocity()
a = a or vector.zero()
if not entity_def._vl_projectile.ignore_gravity then
a = a + vector.new(0,-GRAVITY,0)
end
if entity_def.liquid_drag then
local def = minetest.registered_nodes[minetest.get_node(pos).name]
if def and def.liquidtype ~= "none" then
-- Slow down arrow in liquids
local visc = def.liquid_viscosity or 0
le._viscosity = visc
local vpenalty = math.max(0.1, 0.98 - 0.1 * visc)
if math.abs(v.x) > 0.001 then
v.x = v.x * vpenalty
end
if math.abs(v.z) > 0.001 then
v.z = v.z * vpenalty
end
end
end
end
-- Pass to entity
if v then obj:set_velocity(v) end
if a then obj:set_acceleration(a) end
-- Update projectile yaw to match velocity direction
if v and le and not le._stuck then
local yaw = minetest.dir_to_yaw(v) + YAW_OFFSET
local pitch = math.asin(vector.normalize(v).y)
obj:set_rotation(vector.new(0,yaw,pitch))
end
end
function mod.update_projectile(self, dtime)
if self._removed then return end
-- Workaround for randomly occurring velocity change between projectile creation
-- and the first time step
if self._starting_velocity then
local curr_velocity = self.object:get_velocity()
local distance = vector.distance(curr_velocity, self._starting_velocity)
local length = vector.length(self._starting_velocity)
if length / distance > 1 then
self.object:set_velocity(self._starting_velocity)
end
self._starting_velocity = nil
end
local entity_name = self.name
local entity_def = minetest.registered_entities[entity_name] or {}
local entity_vl_projectile = entity_def._vl_projectile or {}
-- Update entity timer and remove expired projectiles
self.timer = (self.timer or 0) + dtime
local maximum_flight_time = entity_vl_projectile.maximum_time or 300
if (self.timer or 0) > maximum_flight_time then
self.removed = true
self.object:remove()
return
end
-- Run behaviors
local behaviors = entity_vl_projectile.behaviors or {}
for i=1,#behaviors do
if behaviors[i](self, dtime, entity_def, entity_vl_projectile) then
return
end
end
if not self._stuck then
mod.projectile_physics(self.object, entity_def)
end
end
local function damage_particles(pos, is_critical)
if is_critical then
minetest.add_particlespawner({
amount = 15,
time = 0.1,
minpos = vector.offset(pos, -0.5, -0.5, -0.5),
maxpos = vector.offset(pos, 0.5, 0.5, 0.5),
minvel = vector.new(-0.1, -0.1, -0.1),
maxvel = vector.new(0.1, 0.1, 0.1),
minexptime = 1,
maxexptime = 2,
minsize = 1.5,
maxsize = 1.5,
collisiondetection = false,
vertical = false,
texture = "mcl_particles_crit.png^[colorize:#bc7a57:127",
})
end
end
local function random_hit_positions(positions, placement)
if positions == "x" then
return math.random(-4, 4)
elseif positions == "y" then
return math.random(0, 10)
elseif positions == "z" then
if placement == "front" then
return 3
elseif placement == "back" then
return -3
end
end
return 0
end
local function check_hitpoint(hitpoint)
if hitpoint.type ~= "object" then return false end
-- find the closest object that is in the way of the arrow
-- TODO: change this check when adding mob projectiles
if hitpoint.ref:is_player() and enable_pvp then
return true
end
if not hitpoint.ref:is_player() and hitpoint.ref:get_luaentity() then
if (hitpoint.ref:get_luaentity().is_mob or hitpoint.ref:get_luaentity()._hittable_by_projectile) then
return true
end
end
return false
end
local function handle_player_sticking(self, entity_def, projectile_def, entity)
if self._in_player or self._blocked then return end
if not projectile_def.sticks_in_players then return end
minetest.after(150, function()
self._removed = true
self.object:remove()
end)
-- Handle blocking projectiles
if mcl_shields.is_blocking(entity) then
self._blocked = true
self.object:set_velocity(vector.multiply(self.object:get_velocity(), -0.25))
return
end
-- Handle when the projectile hits the player
self._placement = math.random(1, 2)
local placement = self._placement == 1 and "front" or "back"
self._rotation_station = self.placement == 1 and -90 or 90
self._in_player = true
self._y_position = random_hit_positions("y", placement)
self._x_position = random_hit_positions("x", placement)
if self._y_position > 6 and self._x_position < 2 and self._x_position > -2 then
self._attach_parent = "Head"
self._y_position = self._y_position - 6
elseif self._x_position > 2 then
self._attach_parent = "Arm_Right"
self._y_position = self._y_position - 3
self._x_position = self._x_position - 2
elseif self._x_position < -2 then
self._attach_parent = "Arm_Left"
self._y_position = self._y_position - 3
self._x_position = self._x_position + 2
else
self._attach_parent = "Body"
end
self._z_rotation = math.random(-30, 30)
self._y_rotation = math.random(-30, 30)
self.object:set_attach(
entity, self._attach_parent,
vector.new(self._x_position, self._y_position, random_hit_positions("z", placement)),
vector.new(0, self._rotation_station + self._y_rotation, self._z_rotation)
)
end
function mod.burns(self, dtime, entity_def, projectile_def)
mcl_burning.tick(self.object, dtime, self)
-- mcl_burning.tick may remove object immediately
local pos = self.object:get_pos()
if not pos then return true end
-- Handle getting set on fire
local node = minetest.get_node(vector.round(pos))
if not node or node.name == "ignore" then return end
local set_on_fire = minetest.get_item_group(node.name, "set_on_fire")
if set_on_fire ~= 0 then
mcl_burning.set_on_fire(self.object, set_on_fire)
end
end
function mod.has_tracer(self, dtime, entity_def, projectile_def)
local hide_tracer = projectile_def.hide_tracer
if hide_tracer and hide_tracer(self) then return end
-- Add tracer
minetest.add_particlespawner({
amount = 20,
time = .2,
minpos = vector.zero(),
maxpos = vector.zero(),
minvel = vector.new(-0.1,-0.1,-0.1),
maxvel = vector.new(0.1,0.1,0.1),
minexptime = 0.5,
maxexptime = 0.5,
minsize = 2,
maxsize = 2,
attached = self.object,
collisiondetection = false,
vertical = false,
texture = projectile_def.tracer_texture or "mobs_mc_arrow_particle.png",
glow = 1,
})
end
function mod.replace_with_item_drop(self, pos, projectile_def)
local item = self._arrow_item
if not item then
projectile_def = projectile_def or self._vl_projectile
if not projectile_def then return end
item = projectile_def.item
end
if self._collectable and not minetest.is_creative_enabled("") then
local item = minetest.add_item(pos, item)
item:set_velocity(vector.zero())
item:set_yaw(self.object:get_yaw())
end
mcl_burning.extinguish(self.object)
self._removed = true
self.object:remove()
end
local function stuck_on_step(self, dtime, entity_def, projectile_def)
-- Don't process objects that have been removed
local pos = self.object:get_pos()
if not pos then return true end
self._stucktimer = (self._stucktimer or 0) + dtime
if self._stucktimer > STUCK_TIMEOUT then
mcl_burning.extinguish(self.object)
self._removed = true
self.object:remove()
return true
end
-- Drop arrow as item when it is no longer stuck
-- TODO: revist after observer rework
self._stuckrechecktimer = (self._stuckrechecktimer or 0) + dtime
if self._stuckrechecktimer > 1 then
self._stuckrechecktimer = 0
if self._stuckin then
local node = minetest.get_node(self._stuckin)
local node_def = minetest.registered_nodes[node.name]
if node_def and node_def.walkable == false then
mod.replace_with_item_drop(self, pos, projectile_def)
return
end
end
end
-- Pickup arrow if player is nearby (not in Creative Mode)
local objects = minetest.get_objects_inside_radius(pos, 1)
for i = 1,#objects do
obj = objects[i]
if obj:is_player() then
if self._collectable and not minetest.is_creative_enabled(obj:get_player_name()) then
local arrow_item = self._arrow_item
if arrow_item and minetest.registered_items[arrow_item] and obj:get_inventory():room_for_item("main", arrow_item) then
obj:get_inventory():add_item("main", arrow_item)
minetest.sound_play("item_drop_pickup", {
pos = pos,
max_hear_distance = 16,
gain = 1.0,
}, true)
end
end
mcl_burning.extinguish(self.object)
self.object:remove()
return
end
return true
end
end
function mod.sticks(self, dtime, entity_def, projectile_def)
-- Force the projectile to survive collisions (Otherwise, the projectile can't stick in nodes)
projectile_def.survive_collision = true
projectile_def.sticks_in_nodes = true
-- Stuck handling
if self._stuck then
return stuck_on_step(self, dtime, entity_def, projectile_def)
end
end
function mod.collides_with_solids(self, dtime, entity_def, projectile_def)
local pos = self.object:get_pos()
if not pos then return end
-- Don't try to do anything on first update
if not self._last_pos then
self._last_pos = pos
return
end
-- Check if the object can collide with this node
local node = minetest.get_node(pos)
local node_def = minetest.registered_nodes[node.name]
local collides_with = projectile_def.collides_with
if entity_def.physical then
-- Projectile has stopped in one axis, so it probably hit something.
-- This detection is a bit clunky, but sadly, MT does not offer a direct collision detection for us. :-(
local vel = self.object:get_velocity()
if not self._last_velocity then
self._last_velocity = vel
return
end
local delta_v = (vel - self._last_velocity) / vector.length(vel)
if math.abs(delta_v.x) <= 0.1 and math.abs(delta_v.z) <= 0.1 and math.abs(delta_v.y) <= 0.2 then
return
end
self._last_velocity = vel
else
if node_def and not node_def.walkable and (not collides_with or not mcl_util.match_node_to_filter(node.name, collides_with)) then
return
end
end
-- Handle sticking in nodes
if projectile_def.sticks_in_nodes then
local vel = self.object:get_velocity()
local dpos = vector.round(pos) -- digital pos
-- Check for the node to which the arrow is pointing
local dir
if math.abs(vel.y) < 0.00001 then
if self._last_pos.y < pos.y then
dir = vector.new(0, 1, 0)
else
dir = vector.new(0, -1, 0)
end
else
dir = minetest.facedir_to_dir(minetest.dir_to_facedir(minetest.yaw_to_dir(self.object:get_yaw()-YAW_OFFSET)))
end
self._stuckin = vector.add(dpos, dir)
local snode = minetest.get_node(self._stuckin)
local sdef = minetest.registered_nodes[snode.name]
-- If node is non-walkable, unknown or ignore, don't make arrow stuck.
-- This causes a deflection in the engine.
if not sdef or sdef.walkable == false or snode.name == "ignore" then
self._stuckin = nil
if self._deflection_cooloff <= 0 then
-- Lose 1/3 of velocity on deflection
local newvel = vector.multiply(vel, 0.6667)
self.object:set_velocity(newvel)
-- Reset deflection cooloff timer to prevent many deflections happening in quick succession
self._deflection_cooloff = 1.0
end
return
end
-- Node was walkable, make arrow stuck
self._stuck = true
self._stucktimer = 0
self._stuckrechecktimer = 0
self.object:set_velocity(vector.zero())
self.object:set_acceleration(vector.zero())
-- Trigger hits on the node the projectile hit
local hook = sdef._vl_projectile and sdef._vl_projectile.on_collide
if hook then hook(self, self._stuckin, snode, sdef) end
end
-- Call entity collied hook
local hook = projectile_def.on_collide_with_solid
if hook then hook(self, pos, node, node_def) end
-- Call node collided hook
local hook = node_def and node_def._vl_projectile and node_def._vl_projectile.on_collide
if hook then hook(self, pos, node, node_def) end
-- Play sounds
local sounds = projectile_def.sounds or {}
local sound = sounds.on_solid_collision or sounds.on_collision
if type(sound) == "function" then sound = sound(self, entity_def, projectile_def, "node", pos, node, node_def) end
if sound then
local arg2 = table.copy(sound[2])
arg2.pos = pos
minetest.sound_play(sound[1], arg2, sound[3])
end
-- Normally objects should be removed on collision with solids
local survive_collision = projectile_def.survive_collision
if type(survive_collision) == "function" then
survive_collision = survive_collision(self, entity_def, projectile_def, "node", node, node_def)
end
if not survive_collision then
self._removed = true
self.object:remove()
end
-- Done with behaviors
return true
end
local function handle_entity_collision(self, entity_def, projectile_def, object)
local pos = self.object:get_pos()
local dir = vector.normalize(self.object:get_velocity())
-- Check if this is allowed
local allow_punching = projectile_def.allow_punching or true
if type(allow_punching) == "function" then
allow_punching = allow_punching(self, entity_def, projectile_def, object)
end
if DEBUG then
minetest.log("handle_entity_collision("..dump({
self = self,
allow_punching = allow_punching,
entity_def = entity_def,
object = object,
luaentity = object:get_luaentity(),
})..")")
end
if not allow_punching then return end
-- Get damage
local dmg = projectile_def.damage_groups or 0
if type(dmg) == "function" then
dmg = dmg(self, entity_def, projectile_def, object)
end
local object_lua = object:get_luaentity()
-- Apply damage
-- Note: Damage blocking for shields is handled in mcl_shields with an mcl_damage modifier
local do_damage = false
if object:is_player() and projectile_def.damages_players then
do_damage = true
handle_player_sticking(self, entity_def, projectile_def, object)
elseif object_lua and (object_lua.is_mob or object_lua._hittable_by_projectile) then
do_damage = true
end
if do_damage then
object:punch(self.object, 1.0, projectile_def.tool or { full_punch_interval = 1.0, damage_groups = dmg }, dir )
-- Guard against crashes when projectiles get destroyed in response to what it punched
if not self.object:get_pos() then return true end
-- Indicate damage
damage_particles(vector.add(pos, vector.multiply(self.object:get_velocity(), 0.1)), self._is_critical)
-- Light things on fire
if mcl_burning.is_burning(self.object) then
mcl_burning.set_on_fire(object, 5)
end
end
-- Call entity collision hook
local hook = projectile_def.on_collide_with_entity
if hook then hook(self, pos, object) end
-- Call reverse entity collision hook
local other_entity_def = minetest.registered_entities[object.name] or {}
local other_entity_vl_projectile = other_entity_def._vl_projectile or {}
local hook = other_entity_vl_projectile and other_entity_vl_projectile.on_collide
if hook then hook(object, self) end
-- Play sounds
local sounds = projectile_def.sounds or {}
local sound = sounds.on_entity_collion or sounds.on_collision
if type(sound) == "function" then sound = sound(self, entity_def, projectile_def, "entity", object) end
if sound then
local arg2 = table.copy(sound[2])
arg2.pos = pos
minetest.sound_play(sound[1], arg2, sound[3])
end
-- Normally objects should be removed on collision with entities
local survive_collision = projectile_def.survive_collision
if type(survive_collision) == "function" then
survive_collision = survive_collision(self, entity_def, projectile_def, "entity", object)
end
if not survive_collision then
self._removed = true
self.object:remove()
end
return true
end
function mod.collides_with_entities(self, dtime, entity_def, projectile_def)
local pos = self.object:get_pos()
local objects = minetest.get_objects_inside_radius(pos, 1.5)
for i = 1,#objects do
local object = objects[i]
local entity = object:get_luaentity()
if object ~= self.object and (not entity or entity.name ~= self.name) then
if object:is_player() then
return handle_entity_collision(self, entity_def, projectile_def, object)
elseif (entity.is_mob or entity._hittable_by_projectile) then
return handle_entity_collision(self, entity_def, projectile_def, object)
end
end
end
end
function mod.raycast_collides_with_entities(self, dtime, entity_def, projectile_def)
local closest_object, closest_distance
local pos = self.object:get_pos()
if not pos then return end
local arrow_dir = self.object:get_velocity()
--create a raycast from the arrow based on the velocity of the arrow to deal with lag
local raycast = minetest.raycast(pos, vector.add(pos, vector.multiply(arrow_dir, 0.1)), true, false)
for hitpoint in raycast do
if check_hitpoint(hitpoint) then
local hitpoint_ref = hitpoint.ref
local dist = vector.distance(hitpoint_ref:get_pos(), pos)
if not closest_distance or dist < closest_distance then
closest_object = hitpoint_ref
closest_distance = dist
end
end
end
if closest_object then
return handle_entity_collision(self, entity_def, projectile_def, closest_object)
end
end
function mod.create(entity_id, options)
local pos = options.pos
local obj = minetest.add_entity(pos, entity_id, options.staticdata)
-- Set initial velocity and acceleration
local a, v
if options.dir then
v = vector.multiply(options.dir, options.velocity or 0)
a = vector.multiply(v, -math.abs(options.drag or 0))
else
a = vector.zero()
v = a
end
local entity_def = minetest.registered_entities[entity_id]
mod.projectile_physics(obj, entity_def, v, a)
-- Update projectile parameters
local luaentity = obj:get_luaentity()
if options.owner_id then
luaentity._owner = options.owner_id
else
luaentity._owner = mcl_util.get_entity_id(options.owner)
end
luaentity._starting_velocity = obj:get_velocity()
luaentity._vl_projectile = {
extra = options.extra,
}
-- And provide the caller with the created object
return obj
end
function mod.register(name, def)
assert(def._vl_projectile, "vl_projectile.register() requires definition to define _vl_projectile")
assert(def._vl_projectile.behaviors, "vl_projectile.register() requires definition to define _vl_projectile.behaviors")
local behaviors = def._vl_projectile.behaviors
for i = 1,#behaviors do
assert(behaviors[i] and type(behaviors[i]) == "function", "def._vl_projectile.behaviors["..i.." is malformed")
end
if not def.on_step then
def.on_step = mod.update_projectile
end
def._thrower = nil
def._shooter = nil
def._last_pos = nil
minetest.register_entity(name, def)
end

View File

@ -0,0 +1,3 @@
name = vl_projectile
depends = mcl_util
optional_depends = vl_physics, mcl_shields, mcl_burning, mcl_util