---Gets total armor level from 3d armor ---@param player ObjectRef ---@return integer local function get_3d_armor_armor(player) local armor_total = 0 if not player:is_player() or not minetest.get_modpath('3d_armor') or not armor.def[player:get_player_name()] then return armor_total end armor_total = armor.def[player:get_player_name()].level return armor_total end ---Limits number `x` between `min` and `max` values ---@param x integer ---@param min integer ---@param max integer ---@return integer local function limit(x, min, max) return math.min(math.max(x, min), max) end ---Gets collision box ---@param obj ObjectRef ---@return number[] local function get_obj_box(obj) local box if obj:is_player() then box = obj:get_properties().collisionbox or {-0.5, 0.0, -0.5, 0.5, 1.0, 0.5} else box = obj:get_luaentity().collisionbox or {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5} end return box end ---Poison Arrow Effects ---@param tick integer|number ---@param time integer|number ---@param time_left integer|number ---@param arrow_obj ObjectRef ---@param target_obj ObjectRef ---@param old_damage_texture_modifier string ---@param punch_def table function x_bows.poison_effect(tick, time, time_left, arrow_obj, target_obj, old_damage_texture_modifier, punch_def) if not arrow_obj or target_obj:get_hp() <= 0 then return end target_obj:set_properties({damage_texture_modifier = '^[colorize:#00FF0050'}) time_left = time_left + tick if time_left <= time then minetest.after( tick, x_bows.poison_effect, tick, time, time_left, arrow_obj, target_obj, old_damage_texture_modifier, punch_def ) elseif target_obj:is_player() then if x_bows.hbhunger then -- Reset HUD bar color hb.change_hudbar(target_obj, 'health', nil, nil, 'hudbars_icon_health.png', nil, 'hudbars_bar_health.png') end if old_damage_texture_modifier then target_obj:set_properties({damage_texture_modifier = old_damage_texture_modifier}) end -- return else -- local lua_ent = target_obj:get_luaentity() -- if not lua_ent then -- return -- end -- lua_ent[arrow_obj.arrow .. '_active'] = false if old_damage_texture_modifier then target_obj:set_properties({damage_texture_modifier = old_damage_texture_modifier}) end -- return end local _damage = punch_def.tool_capabilities.damage_groups.fleshy if target_obj:get_hp() - _damage > 0 then target_obj:punch( punch_def.puncher, punch_def.time_from_last_punch, punch_def.tool_capabilities ) local target_obj_pos = target_obj:get_pos() if target_obj_pos then x_bows.particle_effect(target_obj_pos, 'arrow_tipped') end end end -- Main Arrow Entity minetest.register_entity('x_bows:arrow_entity', { initial_properties = { visual = 'wielditem', collisionbox = {0, 0, 0, 0, 0, 0}, selectionbox = {0, 0, 0, 0, 0, 0}, physical = false, textures = {'air'}, hp_max = 1 }, ---@param self table ---@param staticdata string on_activate = function(self, staticdata) if not self or not staticdata or staticdata == '' then self.object:remove() return end local _staticdata = minetest.deserialize(staticdata) -- set/reset - do not inherit from previous entity table self._velocity = {x = 0, y = 0, z = 0} self._old_pos = nil self._attached = false self._attached_to = { type = '', pos = nil } self._has_particles = false self._lifetimer = 60 self._nodechecktimer = 0.5 self._is_drowning = false self._in_liquid = false self._poison_arrow = false self._shot_from_pos = self.object:get_pos() self.arrow = _staticdata.arrow self.user = minetest.get_player_by_name(_staticdata.user_name) self._tflp = _staticdata._tflp self._tool_capabilities = _staticdata._tool_capabilities self._is_critical_hit = _staticdata.is_critical_hit self._faster_arrows_multiplier = _staticdata.faster_arrows_multiplier self._add_damage = _staticdata.add_damage if self.arrow == 'x_bows:arrow_diamond_tipped_poison' then self._poison_arrow = true end self.object:set_properties({ textures = {'x_bows:arrow_node'}, infotext = self.arrow }) end, ---@param self table ---@param killer ObjectRef|nil on_death = function(self, killer) if not self._old_pos then self.object:remove() return end minetest.item_drop(ItemStack(self.arrow), nil, vector.round(self._old_pos)) end, ---@param self table ---@param dtime integer|number on_step = function(self, dtime) local pos = self.object:get_pos() self._old_pos = self._old_pos or pos local ray = minetest.raycast(self._old_pos, pos, true, true) local pointed_thing = ray:next() self._lifetimer = self._lifetimer - dtime self._nodechecktimer = self._nodechecktimer - dtime -- adjust pitch when flying if not self._attached then local velocity = self.object:get_velocity() local v_rotation = self.object:get_rotation() local pitch = math.atan2(velocity.y, math.sqrt(velocity.x^2 + velocity.z^2)) self.object:set_rotation({ x = pitch, y = v_rotation.y, z = v_rotation.z }) end -- remove attached arrows after lifetime if self._lifetimer <= 0 then self.object:remove() return end -- add particles only when not attached if not self._attached and not self._in_liquid then self._has_particles = true if self._tflp >= self._tool_capabilities.full_punch_interval then if self._is_critical_hit then x_bows.particle_effect(self._old_pos, 'arrow_crit') elseif self._faster_arrows_multiplier then x_bows.particle_effect(self._old_pos, 'arrow_fast') else x_bows.particle_effect(self._old_pos, 'arrow') end end end -- remove attached arrows after object dies if not self.object:get_attach() and self._attached_to.type == 'object' then self.object:remove() return end -- arrow falls down when not attached to node any more if self._attached_to.type == 'node' and self._attached and self._nodechecktimer <= 0 then local node = minetest.get_node(self._attached_to.pos) self._nodechecktimer = 0.5 if not node then return end if node.name == 'air' then self.object:set_velocity({x = 0, y = -3, z = 0}) self.object:set_acceleration({x = 0, y = -3, z = 0}) -- reset values self._attached = false self._attached_to.type = '' self._attached_to.pos = nil self.object:set_properties({collisionbox = {0, 0, 0, 0, 0, 0}}) return end end while pointed_thing do local ip_pos = pointed_thing.intersection_point local in_pos = pointed_thing.intersection_normal self.pointed_thing = pointed_thing if pointed_thing.type == 'object' and pointed_thing.ref ~= self.object and pointed_thing.ref:get_hp() > 0 and ( (pointed_thing.ref:is_player() and pointed_thing.ref:get_player_name() ~= self.user:get_player_name()) or ( pointed_thing.ref:get_luaentity() and pointed_thing.ref:get_luaentity().physical and pointed_thing.ref:get_luaentity().name ~= '__builtin:item' ) ) and self.object:get_attach() == nil then if pointed_thing.ref:is_player() then minetest.sound_play('x_bows_arrow_successful_hit', { to_player = self.user:get_player_name(), gain = 0.3 }) else minetest.sound_play('x_bows_arrow_hit', { to_player = self.user:get_player_name(), gain = 0.6 }) end -- store these here before punching in case pointed_thing.ref dies local collisionbox = get_obj_box(pointed_thing.ref) local xmin = collisionbox[1] * 100 local ymin = collisionbox[2] * 100 local zmin = collisionbox[3] * 100 local xmax = collisionbox[4] * 100 local ymax = collisionbox[5] * 100 local zmax = collisionbox[6] * 100 self.object:set_velocity({x = 0, y = 0, z = 0}) self.object:set_acceleration({x = 0, y = 0, z = 0}) -- calculate damage local target_armor_groups = pointed_thing.ref:get_armor_groups() local _damage = 0 if self._add_damage then _damage = _damage + self._add_damage end for group, base_damage in pairs(self._tool_capabilities.damage_groups) do _damage = _damage + base_damage * limit(self._tflp / self._tool_capabilities.full_punch_interval, 0.0, 1.0) * ((target_armor_groups[group] or 0) + get_3d_armor_armor(pointed_thing.ref)) / 100.0 end -- crits if self._is_critical_hit then _damage = _damage * 2 end -- knockback local dir = vector.normalize(vector.subtract(self._shot_from_pos, ip_pos)) local distance = vector.distance(self._shot_from_pos, ip_pos) local knockback = minetest.calculate_knockback( pointed_thing.ref, self.object, self._tflp, { full_punch_interval = self._tool_capabilities.full_punch_interval, damage_groups = {fleshy = _damage}, }, dir, distance, _damage ) pointed_thing.ref:add_velocity({ x = dir.x * knockback * -1, y = 7, z = dir.z * knockback * -1 }) pointed_thing.ref:punch( self.object, self._tflp, { full_punch_interval = self._tool_capabilities.full_punch_interval, damage_groups = {fleshy = _damage, knockback = knockback} }, { x = dir.x * -1, y = 7, z = dir.z * -1 } ) -- already dead (entity) if not pointed_thing.ref:get_luaentity() and not pointed_thing.ref:is_player() then self.object:remove() return end -- already dead (player) if pointed_thing.ref:get_hp() <= 0 then if x_bows.hbhunger then -- Reset HUD bar color hb.change_hudbar(pointed_thing.ref, 'health', nil, nil, 'hudbars_icon_health.png', nil, 'hudbars_bar_health.png') end self.object:remove() return end -- attach arrow prepare local rotation = {x = 0, y = 0, z = 0} local position = {x = 0, y = 0, z = 0} if in_pos.x == 1 then -- x = 0 -- y = -90 -- z = 0 rotation.x = math.random(-10, 10) rotation.y = math.random(-100, -80) rotation.z = math.random(-10, 10) position.x = xmax / 10 position.y = math.random(ymin, ymax) / 10 position.z = math.random(zmin, zmax) / 10 elseif in_pos.x == -1 then -- x = 0 -- y = 90 -- z = 0 rotation.x = math.random(-10, 10) rotation.y = math.random(80, 100) rotation.z = math.random(-10, 10) position.x = xmin / 10 position.y = math.random(ymin, ymax) / 10 position.z = math.random(zmin, zmax) / 10 elseif in_pos.y == 1 then -- x = -90 -- y = 0 -- z = -180 rotation.x = math.random(-100, -80) rotation.y = math.random(-10, 10) rotation.z = math.random(-190, -170) position.x = math.random(xmin, xmax) / 10 position.y = ymax / 10 position.z = math.random(zmin, zmax) / 10 elseif in_pos.y == -1 then -- x = 90 -- y = 0 -- z = 180 rotation.x = math.random(80, 100) rotation.y = math.random(-10, 10) rotation.z = math.random(170, 190) position.x = math.random(xmin, xmax) / 10 position.y = ymin / 10 position.z = math.random(zmin, zmax) / 10 elseif in_pos.z == 1 then -- x = 180 -- y = 0 -- z = 180 rotation.x = math.random(170, 190) rotation.y = math.random(-10, 10) rotation.z = math.random(170, 190) position.x = math.random(xmin, xmax) / 10 position.y = math.random(ymin, ymax) / 10 position.z = zmax / 10 elseif in_pos.z == -1 then -- x = -180 -- y = 180 -- z = -180 rotation.x = math.random(-190, -170) rotation.y = math.random(170, 190) rotation.z = math.random(-190, -170) position.x = math.random(xmin, xmax) / 10 position.y = math.random(ymin, ymax) / 10 position.z = zmin / 10 end -- poison arrow if self._poison_arrow then local old_damage_texture_modifier = pointed_thing.ref:get_properties().damage_texture_modifier local punch_def = {} punch_def.puncher = self.object punch_def.time_from_last_punch = self._tflp punch_def.tool_capabilities = { full_punch_interval = self._tool_capabilities.full_punch_interval, damage_groups = {fleshy = _damage, knockback = knockback} } if pointed_thing.ref:is_player() then -- @TODO missing `active` posion arrow check for player (see lua_ent below) if x_bows.hbhunger then -- Set poison bar hb.change_hudbar( pointed_thing.ref, 'health', nil, nil, 'hbhunger_icon_health_poison.png', nil, 'hbhunger_bar_health_poison.png' ) end x_bows.poison_effect(1, 5, 0, self, pointed_thing.ref, old_damage_texture_modifier, punch_def) else -- local lua_ent = pointed_thing.ref:get_luaentity() -- if not lua_ent[self.arrow .. '_active'] or lua_ent[self.arrow .. '_active'] == 'false' then -- lua_ent[self.arrow .. '_active'] = true x_bows.poison_effect(1, 5, 0, self, pointed_thing.ref, old_damage_texture_modifier, punch_def) -- end end end if not x_bows.settings.x_bows_attach_arrows_to_entities and not pointed_thing.ref:is_player() then self.object:remove() return end -- attach arrow self.object:set_attach( pointed_thing.ref, '', position, rotation, true ) self._attached = true self._attached_to.type = pointed_thing.type self._attached_to.pos = position -- remove last arrow when too many already attached local children = {} for _, object in ipairs(pointed_thing.ref:get_children()) do if object:get_luaentity() and object:get_luaentity().name == 'x_bows:arrow_entity' then table.insert(children, object) end end if #children >= 5 then children[1]:remove() end return elseif pointed_thing.type == 'node' and not self._attached then local node = minetest.get_node(pointed_thing.under) local node_def = minetest.registered_nodes[node.name] if not node_def then return end self._velocity = self.object:get_velocity() if node_def.drawtype == 'liquid' and not self._is_drowning then self._is_drowning = true self._in_liquid = true local drag = 1 / (node_def.liquid_viscosity * 6) self.object:set_velocity(vector.multiply(self._velocity, drag)) self.object:set_acceleration({x = 0, y = -1.0, z = 0}) x_bows.particle_effect(self._old_pos, 'bubble') elseif self._is_drowning then self._is_drowning = false if self._velocity then self.object:set_velocity(self._velocity) end self.object:set_acceleration({x = 0, y = -9.81, z = 0}) end if x_bows.mesecons and node.name == 'x_bows:target' then local distance = vector.distance(pointed_thing.under, ip_pos) distance = math.floor(distance * 100) / 100 -- only close to the center of the target will trigger signal if distance < 0.54 then mesecon.receptor_on(pointed_thing.under) minetest.get_node_timer(pointed_thing.under):start(2) end end if node_def.walkable then self.object:set_velocity({x=0, y=0, z=0}) self.object:set_acceleration({x=0, y=0, z=0}) self.object:set_pos(ip_pos) self.object:set_rotation(self.object:get_rotation()) self._attached = true self._attached_to.type = pointed_thing.type self._attached_to.pos = pointed_thing.under self.object:set_properties({collisionbox = {-0.2, -0.2, -0.2, 0.2, 0.2, 0.2}}) -- remove last arrow when too many already attached local children = {} for _, object in ipairs(minetest.get_objects_inside_radius(pointed_thing.under, 1)) do if not object:is_player() and object:get_luaentity() and object:get_luaentity().name == 'x_bows:arrow_entity' then table.insert(children, object) end end if #children >= 5 then children[#children]:remove() end minetest.sound_play('x_bows_arrow_hit', { pos = pointed_thing.under, gain = 0.6, max_hear_distance = 16 }) return end end pointed_thing = ray:next() end self._old_pos = pos end, ---@param self table ---@param puncher ObjectRef|nil ---@param time_from_last_punch number|integer|nil ---@param tool_capabilities ToolCapabilitiesDef|nil ---@param dir Vector ---@param damage number|integer on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage) local wood_sound_def = default.node_sound_wood_defaults() minetest.sound_play(wood_sound_def.dig.name, { pos = self.object:get_pos(), gain = wood_sound_def.dig.gain }) return false end, })