Compare commits

...

8 Commits

14 changed files with 1385 additions and 1170 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, ...)
@ -112,24 +115,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)
@ -140,119 +125,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
@ -410,61 +282,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)
@ -736,243 +553,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
@ -1018,33 +598,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

View File

@ -0,0 +1,462 @@
-- 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 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
-- 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
---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
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
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

@ -496,7 +496,7 @@ mcl_mobs.register_mob("mobs_mc:rover", {
-- compat -- compat
minetest.register_entity("mobs_mc:enderman", { minetest.register_entity("mobs_mc:enderman", {
on_activate = function(self, staticdata, dtime) on_activate = function(self, staticdata, dtime)
minetest.add_entity(self.object:get_pos(), "mobs_mc:rover", staticdata) local obj = minetest.add_entity(self.object:get_pos(), "mobs_mc:rover", staticdata)
obj:set_properties({ obj:set_properties({
mesh = "vl_rover.b3d", mesh = "vl_rover.b3d",
textures = { "vl_mobs_rover.png^vl_mobs_rover_face.png" }, textures = { "vl_mobs_rover.png^vl_mobs_rover_face.png" },

View File

@ -56,7 +56,65 @@ 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 function spawn_item(self, pos)
if not minetest.is_creative_enabled("") then
local item = minetest.add_item(pos, "mcl_bows:arrow")
item:set_velocity(vector.new(0, 0, 0))
item:set_yaw(self.object:get_yaw())
end
mcl_burning.extinguish(self.object)
self.object:remove()
end
local function stuck_arrow_on_step(self, dtime)
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
local pos = self.object:get_pos()
-- 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
end
vl_projectile.register("mcl_bows:arrow_entity", {
physical = true, physical = true,
pointable = false, pointable = false,
visual = "mesh", visual = "mesh",
@ -67,7 +125,10 @@ local ARROW_ENTITY={
collide_with_objects = false, collide_with_objects = false,
_fire_damage_resistant = true, _fire_damage_resistant = true,
_lastpos={}, _save_fields = {
"last_pos", "startpos", "damage", "is_critical", "stuck", "stuckin", "stuckin_player",
},
_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,282 +142,41 @@ 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") damage_groups = function(self)
item:set_velocity(vector.new(0, 0, 0)) return { fleshy = self._damage }
item:set_yaw(self.object:get_yaw()) end,
end behaviors = {
mcl_burning.extinguish(self.object) vl_projectile.collides_with_solids,
self.object:remove() vl_projectile.raycast_collides_with_entities,
end },
allow_punching = function(self, entity_def, projectile_def, entity)
local lua = entity:get_luaentity()
if lua and lua.name == "mobs_mc:rover" then return false end
local function damage_particles(pos, is_critical) return true
if is_critical then end,
minetest.add_particlespawner({ sounds = {
amount = 15, on_entity_collision = function(self, _, _, obj)
time = 0.1, if obj:is_player() then
minpos = vector.offset(pos, -0.5, -0.5, -0.5), return {{name="mcl_bows_hit_player", gain=0.1}, {to_player=self._shooter:get_player_name()}, true}
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
local pos = self.object:get_pos()
local dpos = vector.round(vector.new(pos)) -- digital pos
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 end
mcl_burning.extinguish(self.object)
self.object:remove() return {{name="mcl_bows_hit_other", gain=0.3}, {pos=self.object:get_pos(), max_hear_distance=16}, true}
return
end end
end },
on_collide_with_solid = function(self, pos, node, node_def)
local def = node_def
local vel = self.object:get_velocity()
local dpos = vector.round(vector.new(pos)) -- digital pos
-- 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
self._deflection_cooloff = self._deflection_cooloff - dtime
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 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 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 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 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 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
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 -- Check for the node to which the arrow is pointing
local dir local dir
if math.abs(vel.y) < 0.00001 then if math.abs(vel.y) < 0.00001 then
if self._lastpos.y < pos.y then if self._last_pos.y < pos.y then
dir = vector.new(0, 1, 0) dir = vector.new(0, 1, 0)
else else
dir = vector.new(0, -1, 0) dir = vector.new(0, -1, 0)
@ -380,50 +200,147 @@ function ARROW_ENTITY.on_step(self, dtime)
-- Reset deflection cooloff timer to prevent many deflections happening in quick succession -- Reset deflection cooloff timer to prevent many deflections happening in quick succession
self._deflection_cooloff = 1.0 self._deflection_cooloff = 1.0
end end
return
end
-- 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,
on_collide_with_entity = function(self, pos, obj)
local is_player = obj:is_player()
local lua = obj:get_luaentity()
-- Make sure collision is valid
if obj == self._shooter then
if self._time_in_air < 1.02 then return end
else else
if not (is_player or (lua and (lua.is_mob or lua._hittable_by_projectile))) then
return
end
end
-- Node was walkable, make arrow stuck if obj:get_hp() > 0 then
self._stuck = true -- Check if there is no solid node between arrow and object
self._stucktimer = 0 -- TODO: remove. this code should never occur if vl_projectile is working correctly
self._stuckrechecktimer = 0 local ray = minetest.raycast(self.object:get_pos(), obj:get_pos(), true)
for pointed_thing in ray do
self.object:set_velocity(vector.new(0, 0, 0)) if pointed_thing.type == "object" and pointed_thing.ref == obj then
self.object:set_acceleration(vector.new(0, 0, 0)) -- Target reached! We can proceed now.
break
minetest.sound_play({name="mcl_bows_hit_other", gain=0.3}, {pos=self.object:get_pos(), max_hear_distance=16}, true) elseif pointed_thing.type == "node" then
local nn = minetest.get_node(minetest.get_pointed_thing_position(pointed_thing)).name
if mcl_burning.is_burning(self.object) and snode.name == "mcl_tnt:tnt" then local def = minetest.registered_nodes[nn]
tnt.ignite(self._stuckin) 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 end
-- Ignite Campfires if lua then
if mod_campfire and mcl_burning.is_burning(self.object) and minetest.get_item_group(snode.name, "campfire") ~= 0 then local entity_name = lua.name
mcl_campfires.light_campfire(self._stuckin) -- Achievement for hitting skeleton, wither skeleton or stray (TODO) with an arrow at least 50 meters away
end -- 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
-- Activate target if self._shooter and self._shooter:is_player() and vector.distance(pos, self._startpos) >= 20 then
if mod_target and snode.name == "mcl_target:target_off" then if mod_awards and (entity_name == "mobs_mc:skeleton" or entity_name == "mobs_mc:stray" or entity_name == "mobs_mc:witherskeleton") then
mcl_target.hit(self._stuckin, 1) --10 redstone ticks awards.unlock(self._shooter:get_player_name(), "mcl:snipeSkeleton")
end 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 end
end end
elseif (def and def.liquidtype ~= "none") then
-- Slow down arrow in liquids if not obj:is_player() then
local v = def.liquid_viscosity mcl_burning.extinguish(self.object)
if not v then if self._piercing == 0 then
v = 0 self.object:remove()
end
end end
--local old_v = self._viscosity end
},
on_step = function(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 + dtime
local pos = self.object:get_pos()
--local dpos = vector.round(vector.new(pos)) -- digital pos
--local node = minetest.get_node(dpos)
if self._stuck then
return stuck_arrow_on_step(self, dtime)
end
-- Add tracer
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
if self._deflection_cooloff > 0 then
self._deflection_cooloff = self._deflection_cooloff - dtime
end
-- TODO: change to use vl_physics
-- TODO: move to vl_projectile
local def = minetest.registered_nodes[minetest.get_node(pos).name]
if def and def.liquidtype ~= "none" then
-- Slow down arrow in liquids
local v = def.liquid_viscosity or 0
self._viscosity = v self._viscosity = v
local vpenalty = math.max(0.1, 0.98 - 0.1 * v) local vpenalty = math.max(0.1, 0.98 - 0.1 * v)
local vel = self.object:get_velocity()
if math.abs(vel.x) > 0.001 then if math.abs(vel.x) > 0.001 then
vel.x = vel.x * vpenalty vel.x = vel.x * vpenalty
end end
@ -432,79 +349,70 @@ function ARROW_ENTITY.on_step(self, dtime)
end end
self.object:set_velocity(vel) self.object:set_velocity(vel)
end end
end
-- Update yaw -- Process as projectile
if not self._stuck then vl_projectile.update_projectile(self, dtime)
-- Update yaw
local vel = self.object:get_velocity() local vel = self.object:get_velocity()
local yaw = minetest.dir_to_yaw(vel)+YAW_OFFSET if vel and not self._stuck then
local pitch = dir_to_pitch(vel) local yaw = minetest.dir_to_yaw(vel)+YAW_OFFSET
self.object:set_rotation({ x = 0, y = yaw, z = pitch }) local pitch = dir_to_pitch(vel)
end self.object:set_rotation({ x = 0, y = yaw, z = pitch })
-- 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
self._stuckrechecktimer = 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,
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 end
out.stuckstarttime = minetest.get_gametime() - self._stucktimer end,
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) -- Force recheck of stuck arrows when punched.
self._time_in_air = 1.0 -- Otherwise, punching has no effect.
local data = minetest.deserialize(staticdata) on_punch = function(self)
if data then if self._stuck 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
mcl_burning.extinguish(self.object)
self.object:remove()
return
end
end
-- Perform a stuck recheck on the next step.
self._stuckrechecktimer = STUCK_RECHECK_TIME self._stuckrechecktimer = STUCK_RECHECK_TIME
end
self._stuckin = data.stuckin 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
-- Get the remaining arrow state if self._stuck then
self._lastpos = data.lastpos -- If _stucktimer is missing for some reason, assume the maximum
self._startpos = data.startpos if not self._stucktimer then
self._damage = data.damage self._stucktimer = ARROW_TIMEOUT
self._is_critical = data.is_critical 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,
on_activate = function(self, staticdata, dtime_s)
self.object:set_armor_groups({ immortal = 1 })
self._time_in_air = 1.0
local data = minetest.deserialize(staticdata)
if not data then return end
-- Restore arrow state
local save_fields = self._save_fields
for i = 1,#save_fields do
local field = save_fields[i]
self["_"..field] = data[field]
end
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
end
-- Perform a stuck recheck on the next step.
self._stuckrechecktimer = STUCK_RECHECK_TIME
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
@ -515,9 +423,8 @@ function ARROW_ENTITY.on_activate(self, staticdata, dtime_s)
if data.stuckin_player then if data.stuckin_player then
self.object:remove() self.object:remove()
end end
end end,
self.object:set_armor_groups({ immortal = 1 }) })
end
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 +435,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",
@ -544,3 +449,4 @@ end
if minetest.get_modpath("doc_identifier") then if minetest.get_modpath("doc_identifier") then
doc.sub.identifier.register_object("mcl_bows:arrow_entity", "craftitems", "mcl_bows:arrow") doc.sub.identifier.register_object("mcl_bows:arrow_entity", "craftitems", "mcl_bows:arrow")
end end

View File

@ -76,6 +76,7 @@ function mcl_bows.shoot_arrow(arrow_item, pos, dir, yaw, shooter, power, damage,
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_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 obj:get_luaentity().player == "" then

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
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

@ -0,0 +1,69 @@
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(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 = 16,
on_use = mcl_throwing.get_player_throw_function("mcl_throwing:egg_entity"),
_on_dispense = mcl_throwing.dispense_function,
groups = { craftitem = 1 },
})
mcl_throwing.register_throwable_object("mcl_throwing:egg", "mcl_throwing:egg_entity", 22)
minetest.register_entity("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,
},
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
-- 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 return end
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 return end
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,
sounds = {
on_collision = {"mcl_throwing_egg_impact", {max_hear_distance=10, gain=0.5}, true}
},
},
})

View File

@ -0,0 +1,131 @@
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(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 },
})
mcl_throwing.register_throwable_object("mcl_throwing:ender_pearl", "mcl_throwing:ender_pearl_entity", 22)
-- Ender pearl entity
minetest.register_entity("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,
},
collides_with = {
"mcl_core:vine", "mcl_core:deadbush",
"group:flower", "group:sapling",
"group:plant", "group:mushroom",
},
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
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 = {x=0, y=0, z=0}
local v = self.object:get_velocity()
if node_def and node_def.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
},
})

View File

@ -17,21 +17,18 @@ function mcl_throwing.register_throwable_object(name, entity, velocity)
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 = thrower,
if thrower then dir = dir,
obj:get_luaentity()._thrower = thrower velocity = velocity,
end drag = 3,
})
obj:get_luaentity()._thrower = thrower
return obj return obj
end end
@ -71,10 +68,11 @@ 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
end end
end end
dofile(modpath.."/register.lua") dofile(modpath.."/register.lua")

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,330 +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={x=pos.x, y=pos.y, z=pos.z} -- 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 with chance to spawn chicks
if self._lastpos.x then
if (def and def.walkable) or not def then
-- 1/8 chance to spawn a chick
-- FIXME: Chicks have a quite good chance to spawn in walls
local r = math.random(1,8)
if r == 1 then
mcl_mobs.spawn_child(self._lastpos, "mobs_mc:chicken")
-- BONUS ROUND: 1/32 chance to spawn 3 additional chicks
local r = math.random(1,32)
if r == 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
end
-- Destroy when hitting a mob or player (no chick spawning)
if check_object_hit(self, pos, 0) then
minetest.sound_play("mcl_throwing_egg_impact", { pos = self.object:get_pos(), max_hear_distance=10, gain=0.5 }, true)
self.object:remove()
return
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
-- 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={x=pos.x, y=pos.y, z=pos.z} -- 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 = 16,
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 = 16,
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,75 @@
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
local S = minetest.get_translator(modname)
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 = 16,
groups = { weapon_ranged = 1 },
on_use = mcl_throwing.get_player_throw_function("mcl_throwing:snowball_entity"),
_on_dispense = mcl_throwing.dispense_function,
})
mcl_throwing.register_throwable_object("mcl_throwing:snowball", "mcl_throwing:snowball_entity", 22)
-- 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.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
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,
},
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 },
},
})

View File

@ -0,0 +1,341 @@
local mod = {}
vl_projectile = mod
local GRAVITY = tonumber(minetest.settings:get("movement_gravity"))
local enable_pvp = minetest.settings:get_bool("enable_pvp")
function mod.update_projectile(self, dtime)
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
self.timer = (self.timer or 0) + dtime
-- Run behaviors
local behaviors = entity_vl_projectile.behaviors or {}
for i=1,#behaviors do
local behavior = behaviors[i]
if behavior(self, dtime, entity_def, entity_vl_projectile) then
return
end
end
end
local function no_op()
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)
end
if placement == "front" and positions == "z" then
return 3
elseif placement == "back" and positions == "z" then
return -3
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
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.object:remove()
end)
-- Handle blocking projectiles
if mcl_shields.is_blocking(obj) 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
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)
)
end
function mod.collides_with_solids(self, dtime, entity_def, projectile_def)
local pos = self.object:get_pos()
-- 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( (math.abs(vel.x) < 0.0001) or (math.abs(vel.z) < 0.0001) or (math.abs(vel.y) < 0.00001) ) then
self._last_pos = pos
return
end
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
self._last_pos = pos
return
end
end
-- Call entity collied hook
local hook = projectile_def.on_collide_with_solid or no_op
hook(self, pos, node, node_def)
-- Call node collided hook
local hook = (node_def._vl_projectile or {}).on_collide or no_op
hook(self, pos, node, node_def)
-- Play sounds
local sounds = projectile_def.sounds or {}
local sound = sounds.on_solid_collision or sounds.on_collision
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
if not projectile_def.survive_collision then
self.object:remove()
end
-- Done with behaviors
return true
end
local function handle_entity_collision(self, entity_def, projectile_def, entity)
local pos = self.object:get_pos()
local dir = vector.normalize(self.object:get_velocity())
local self_vl_projectile = self._vl_projectile
-- Allow punching
local allow_punching = projectile_def.allow_punching or true
if type(allow_punching) == "function" then
allow_punching = allow_punching(self, entity_def, projectile_def, entity)
end
print("allow_punching="..tostring(allow_punching))
if allow_punching then
-- Get damage
local dmg = projectile_def.damage_groups or 0
if type(dmg) == "function" then
dmg = dmg(self, entity_def, projectile_def, entity)
end
local entity_lua = entity:get_luaentity()
-- Apply damage
-- Note: Damage blocking for shields is handled in mcl_shields with an mcl_damage modifier
local do_damage = false
if entity:is_player() and projectile_def.hits_players and self_vl_projectile.owner ~= hit:get_player_name() then
do_damage = true
handle_player_sticking(self, entity_def, projectile_def, entity)
elseif entity_lua and (entity_lua.is_mob == true or entity_lua._hittable_by_projectile) and (self_vl_projectile.owner ~= entity) then
do_damage = true
entity:punch(self.object, 1.0, projectile_def.tool or { full_punch_interval = 1.0, damage_groups = dmg }, dir )
end
if do_damage then
entity:punch(self.object, 1.0, projectile_def.tool or { full_punch_interval = 1.0, damage_groups = dmg }, dir )
-- 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(obj, 5)
end
end
end
-- Call entity collision hook
(projectile_def.on_collide_with_entity or no_op)(self, pos, entity)
-- Call reverse entity collision hook
local other_entity_def = minetest.registered_entities[entity.name] or {}
local other_entity_vl_projectile = other_entity_def._vl_projectile or {}
local hook = (other_entity_vl_projectile or {}).on_collide or no_op
hook(entity, self)
-- Play sounds
local sounds = (projectile_def.sounds or {})
local sound = sounds.on_entity_collide or sounds.on_collision
if type(sound) == "function" then sound = sound(self, entity_def, projectile_def, entity)
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
if not projectile_def.survive_collision then
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 hit = nil
local owner = self._vl_projectile.owner
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 entity and entity.name ~= self.object:get_luaentity().name then
if object:is_player() and owner ~= object:get_player_name() then
return handle_entity_collision(self, entity_def, projectile_def, object)
elseif (entity.is_mob == true or entity._hittable_by_projectile) and (owner ~= object) 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
local closest_distance
local pos = self.object:get_pos()
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 obj = minetest.add_entity(options.pos, entity_id, options.staticdata)
-- Set initial velocoty and acceleration
obj:set_velocity(vector.multiply(options.dir or vector.zero(), options.velocity or 0))
obj:set_acceleration(vector.add(
vector.multiply(options.dir or vector.zero(), -math.abs(options.drag)),
vector.new(0,-GRAVITY,0)
))
-- Update projectile parameters
local luaentity = obj:get_luaentity()
luaentity._vl_projectile = {
owner = options.owner,
extra = options.extra,
}
-- Make the update function easy to get to
luaentity.update_projectile = mod.update_projectile
-- And provide the caller with the created object
return obj
end
function mod.register(name, def)
assert(def._vl_projectile)
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,2 @@
name = vl_projectile
depends = mcl_util