-- X Bows -- by SaKeL minetest = minetest--[[@as Minetest]] ItemStack = ItemStack--[[@as ItemStack]] vector = vector--[[@as Vector]] default = default--[[@as MtgDefault]] math.randomseed(tonumber(tostring(os.time()):reverse():sub(1, 9))--[[@as number]]) local mod_start_time = minetest.get_us_time() local bow_charged_timer = 0 ---x_bows main class ---@class XBows x_bows = { pvp = minetest.settings:get_bool('enable_pvp') or false, creative = minetest.settings:get_bool('creative_mode') or false, mesecons = minetest.get_modpath('mesecons'), hbhunger = minetest.get_modpath('hbhunger'), playerphysics = minetest.get_modpath('playerphysics'), player_monoids = minetest.get_modpath('player_monoids'), registered_arrows = {}, registered_bows = {}, registered_quivers = {}, player_bow_sneak = {}, settings = { x_bows_attach_arrows_to_entities = minetest.settings:get_bool('x_bows_attach_arrows_to_entities', false) }, quiver = { hud_item_ids = {}, after_job = {} }, charge_sound_after_job = {} } ---Shorthand for checking creative priv ---@param name string ---@return boolean function x_bows.is_creative(name) return x_bows.creative or minetest.check_player_privs(name, {creative = true}) end ---Reset charged bow to uncharged bow, this will return the arrow item to the inventory also ---@param player ObjectRef Player Ref ---@param includeWielded? boolean Will include reset for wielded bow also. default: `false` ---@return nil local function reset_charged_bow(player, includeWielded) local _includeWielded = includeWielded or false local inv = player:get_inventory() if inv and inv:contains_item('main', 'x_bows:bow_wood_charged') then local inv_list = inv:get_list('main') for i, st in ipairs(inv_list) do local reset = _includeWielded or player:get_wield_index() ~= i if not st:is_empty() and x_bows.registered_bows[st:get_name()] and reset then local item_meta = st:get_meta() local arrow_itemstack = ItemStack(minetest.deserialize(item_meta:get_string('arrow_itemstack_string'))) -- return arrow if arrow_itemstack and not x_bows.is_creative(player:get_player_name()) then if inv:room_for_item('main', {name=arrow_itemstack:get_name()}) then inv:add_item('main', arrow_itemstack:get_name()) else minetest.item_drop(ItemStack({name=arrow_itemstack:get_name(), count=1}), player, player:get_pos()) end end -- reset bow to uncharged bow inv:set_stack('main', i, ItemStack({ name=x_bows.registered_bows[st:get_name()].name, count=st:get_count(), wear=st:get_wear() })) end end end end minetest.register_on_joinplayer(function(player) reset_charged_bow(player, true) x_bows.quiver.close_quiver(player) end) ---Register bows ---@param name string ---@param def table ---@return boolean|nil function x_bows.register_bow(name, def) if name == nil or name == '' then return false end def.name = 'x_bows:' .. name def.name_charged = 'x_bows:' .. name .. '_charged' def.description = def.description or name def.uses = def.uses or 150 x_bows.registered_bows[def.name_charged] = def -- not charged bow minetest.register_tool(def.name, { description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Critical Arrow Chance: ' .. (1 / def.crit_chance) * 100 .. '%'), inventory_image = def.inventory_image or 'x_bows_bow_wood.png', on_place = x_bows.load, on_secondary_use = x_bows.load, groups = {bow = 1, flammable = 1} }) -- charged bow minetest.register_tool(def.name_charged, { description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Critical Arrow Chance: ' .. (1 / def.crit_chance) * 100 .. '%'), inventory_image = def.inventory_image_charged or 'x_bows_bow_wood_charged.png', on_use = x_bows.shoot, groups = {bow = 1, flammable = 1, not_in_creative_inventory = 1}, on_drop = function(itemstack, dropper, pos) local item_meta = itemstack:get_meta() local arrow_itemstack = ItemStack(minetest.deserialize(item_meta:get_string('arrow_itemstack_string'))) -- return arrow if arrow_itemstack and not x_bows.is_creative(dropper:get_player_name()) then minetest.item_drop(ItemStack({name=arrow_itemstack:get_name(), count=1}), dropper, {x=pos.x + 0.5, y=pos.y + 0.5, z=pos.z + 0.5}) end itemstack:set_name(def.name) -- returns leftover itemstack return minetest.item_drop(itemstack, dropper, pos) end }) -- recipes if def.recipe then minetest.register_craft({ output = def.name, recipe = def.recipe }) end end ---Register arrows ---@param name string ---@param def table ---@return boolean|nil function x_bows.register_arrow(name, def) if name == nil or name == '' then return false end def.name = 'x_bows:' .. name def.description = def.description or name x_bows.registered_arrows[def.name] = def minetest.register_craftitem('x_bows:' .. name, { description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Damage: ' .. def.tool_capabilities.damage_groups.fleshy) .. '\n' .. minetest.colorize('#00BFFF', 'Charge Time: ' .. def.tool_capabilities.full_punch_interval .. 's'), short_description = def.description, inventory_image = def.inventory_image, groups = {arrow = 1, flammable = 1} }) -- recipes if def.craft then minetest.register_craft({ output = def.name ..' ' .. (def.craft_count or 4), recipe = def.craft }) end end ---Register quivers ---@param name string ---@param def table ---@return boolean|nil function x_bows.register_quiver(name, def) if name == nil or name == '' then return false end def.name = 'x_bows:' .. name def.name_open = 'x_bows:' .. name .. '_open' def.description = def.description or name def.uses = def.uses or 150 x_bows.registered_quivers[def.name] = def ---closed quiver minetest.register_tool(def.name, { description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.faster_arrows) * 100 .. '%') .. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.add_damage), short_description = def.short_description .. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.faster_arrows) * 100 .. '%') .. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.add_damage), inventory_image = def.inventory_image or 'x_bows_quiver.png', wield_image = def.wield_image or 'x_bows_quiver.png', groups = {quiver = 1, flammable = 1}, on_secondary_use = function(itemstack, user, pointed_thing) return x_bows.open_quiver(itemstack, user) end, ---@param itemstack ItemStack ---@param placer ObjectRef ---@param pointed_thing PointedThingDef ---@return ItemStack|nil on_place = function(itemstack, placer, pointed_thing) if pointed_thing.under then local node = minetest.get_node(pointed_thing.under) local node_def = minetest.registered_nodes[node.name] if node_def and node_def.on_rightclick then return node_def.on_rightclick(pointed_thing.under, node, placer, itemstack, pointed_thing) end end return x_bows.open_quiver(itemstack, placer) end }) ---open quiver minetest.register_tool(def.name_open, { description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.faster_arrows) * 100 .. '%') .. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.add_damage), short_description = def.short_description .. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.faster_arrows) * 100 .. '%') .. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.add_damage), inventory_image = def.inventory_image_open or 'x_bows_quiver_open.png', wield_image = def.wield_image_open or 'x_bows_quiver_open.png', groups = {quiver = 1, flammable = 1, not_in_creative_inventory = 1}, ---@param itemstack ItemStack ---@param dropper ObjectRef|nil ---@param pos Vector ---@return ItemStack on_drop = function (itemstack, dropper, pos) local replace_item = x_bows.quiver.get_replacement_item(itemstack, 'x_bows:quiver') return minetest.item_drop(replace_item, dropper, pos) end }) -- recipes if def.recipe then minetest.register_craft({ output = def.name, recipe = def.recipe }) end end ---Loads bow ---@param itemstack ItemStack ---@param user ObjectRef ---@param pointed_thing PointedThingDef ---@return ItemStack function x_bows.load(itemstack, user, pointed_thing) local player_name = user:get_player_name() local inv = user:get_inventory()--[[@as InvRef]] local inv_list = inv:get_list('main') local bow_name = itemstack:get_name() local bow_def = x_bows.registered_bows[bow_name .. '_charged'] ---@alias ItemStackArrows {["stack"]: ItemStack, ["idx"]: number|integer}[] ---@type ItemStackArrows local itemstack_arrows = {} ---trigger right click event if pointed item has one if pointed_thing.under then local node = minetest.get_node(pointed_thing.under) local node_def = minetest.registered_nodes[node.name] if node_def and node_def.on_rightclick then return node_def.on_rightclick(pointed_thing.under, node, user, itemstack, pointed_thing) end end ---find itemstack arrow in quiver local quiver_result = x_bows.quiver.get_itemstack_arrow_from_quiver(user) local itemstack_arrow = quiver_result.found_arrow_stack if itemstack_arrow then local itemstack_arrow_meta = itemstack_arrow:get_meta() itemstack_arrow_meta:set_int('is_arrow_from_quiver', 1) itemstack_arrow_meta:set_string('quiver_name', quiver_result.quiver_name) itemstack_arrow_meta:set_string('quiver_id', quiver_result.quiver_id) else x_bows.quiver.remove_hud(user) ---find itemstack arrow in players inventory for i, st in ipairs(inv_list) do if not st:is_empty() and x_bows.registered_arrows[st:get_name()] then table.insert(itemstack_arrows, {stack = st, idx = i}) end end -- take 1st found arrow in the list itemstack_arrow = #itemstack_arrows > 0 and itemstack_arrows[1].stack or nil end if itemstack_arrow and bow_def then local _tool_capabilities = x_bows.registered_arrows[itemstack_arrow:get_name()].tool_capabilities ---@param v_user ObjectRef ---@param v_bow_name string ---@param v_itemstack_arrow ItemStack ---@param v_inv InvRef ---@param v_itemstack_arrows ItemStackArrows minetest.after(0, function(v_user, v_bow_name, v_itemstack_arrow, v_inv, v_itemstack_arrows) local wielded_item = v_user:get_wielded_item() if wielded_item:get_name() == v_bow_name then local wielded_item_meta = wielded_item:get_meta() local v_itemstack_arrow_meta = v_itemstack_arrow:get_meta() wielded_item_meta:set_string('arrow_itemstack_string', minetest.serialize(v_itemstack_arrow:to_table())) wielded_item_meta:set_string('time_load', tostring(minetest.get_us_time())) wielded_item:set_name(v_bow_name .. '_charged') v_user:set_wielded_item(wielded_item) if not x_bows.is_creative(v_user:get_player_name()) and v_itemstack_arrow_meta:get_int('is_arrow_from_quiver') ~= 1 then v_itemstack_arrow:take_item() v_inv:set_stack('main', v_itemstack_arrows[1].idx, v_itemstack_arrow) end end end, user, bow_name, itemstack_arrow, inv, itemstack_arrows) ---stop previous charged sound after job if x_bows.charge_sound_after_job[player_name] then for _, v in pairs(x_bows.charge_sound_after_job[player_name]) do v:cancel() end x_bows.charge_sound_after_job[player_name] = {} else x_bows.charge_sound_after_job[player_name] = {} end ---sound plays when charge time reaches full punch interval time table.insert(x_bows.charge_sound_after_job[player_name], minetest.after(_tool_capabilities.full_punch_interval, function(v_user, v_bow_name) local wielded_item = v_user:get_wielded_item() local wielded_item_name = wielded_item:get_name() if wielded_item_name == v_bow_name .. '_charged' then minetest.sound_play('x_bows_bow_loaded', { to_player = v_user:get_player_name(), gain = 0.6 }) end end, user, bow_name)) minetest.sound_play('x_bows_bow_load', { to_player = player_name, gain = 0.6 }) return itemstack end return itemstack end ---Shoots the bow ---@param itemstack ItemStack ---@param user ObjectRef ---@param pointed_thing? PointedThingDef ---@return ItemStack function x_bows.shoot(itemstack, user, pointed_thing) local time_shoot = minetest.get_us_time(); local meta = itemstack:get_meta() local time_load = tonumber(meta:get_string('time_load')) local tflp = (time_shoot - time_load) / 1000000 ---@type ItemStack local arrow_itemstack = ItemStack(minetest.deserialize(meta:get_string('arrow_itemstack_string'))) local arrow_itemstack_meta = arrow_itemstack:get_meta() local arrow_name = arrow_itemstack:get_name() local is_arrow_from_quiver = arrow_itemstack_meta:get_int('is_arrow_from_quiver') local quiver_name = arrow_itemstack_meta:get_string('quiver_name') local quiver_id = arrow_itemstack_meta:get_string('quiver_id') local detached_inv = x_bows.quiver.get_or_create_detached_inv( quiver_id, user:get_player_name() ) if is_arrow_from_quiver == 1 then x_bows.quiver.udate_or_create_hud(user, detached_inv:get_list('main')) else x_bows.quiver.remove_hud(user) end if not x_bows.registered_arrows[arrow_name] then return itemstack end local bow_name_charged = itemstack:get_name() local bow_name = x_bows.registered_bows[bow_name_charged].name local uses = x_bows.registered_bows[bow_name_charged].uses local crit_chance = x_bows.registered_bows[bow_name_charged].crit_chance local _tool_capabilities = x_bows.registered_arrows[arrow_name].tool_capabilities local quiver_xbows_def = x_bows.registered_quivers[quiver_name] local staticdata = { arrow = arrow_name, user_name = user:get_player_name(), is_critical_hit = false, _tool_capabilities = _tool_capabilities, _tflp = tflp, } ---crits, only on full punch interval if crit_chance and crit_chance > 1 and tflp >= _tool_capabilities.full_punch_interval then if math.random(1, crit_chance) == 1 then staticdata.is_critical_hit = true end end ---speed multiply if quiver_xbows_def and quiver_xbows_def.faster_arrows and quiver_xbows_def.faster_arrows > 1 then staticdata.faster_arrows_multiplier = quiver_xbows_def.faster_arrows end ---add damage if quiver_xbows_def and quiver_xbows_def.add_damage and quiver_xbows_def.add_damage > 1 then staticdata.add_damage = quiver_xbows_def.add_damage end ---sound local sound_name = 'x_bows_bow_shoot' if staticdata.is_critical_hit then sound_name = 'x_bows_bow_shoot_crit' end meta:set_string('arrow_itemstack_string', '') itemstack:set_name(bow_name) local pos = user:get_pos() local dir = user:get_look_dir() local obj = minetest.add_entity( { x = pos.x, y = pos.y + 1.5, z = pos.z }, 'x_bows:arrow_entity', minetest.serialize(staticdata) ) if not obj then return itemstack end local strength_multiplier = tflp if strength_multiplier > _tool_capabilities.full_punch_interval then strength_multiplier = 1 ---faster arrow, only on full punch interval if staticdata.faster_arrows_multiplier then strength_multiplier = strength_multiplier + (strength_multiplier / staticdata.faster_arrows_multiplier) end end local strength = 30 * strength_multiplier obj:set_velocity(vector.multiply(dir, strength)) obj:set_acceleration({x = dir.x * -3, y = -10, z = dir.z * -3}) obj:set_yaw(minetest.dir_to_yaw(dir)) if not x_bows.is_creative(user:get_player_name()) then itemstack:add_wear(65535 / uses) end minetest.sound_play(sound_name, { gain = 0.3, pos = user:get_pos(), max_hear_distance = 10 }) return itemstack end ---Arrow particle ---@param pos Vector ---@param type 'arrow' | 'arrow_crit' | 'bubble' | 'arrow_tipped' ---@return number|nil function x_bows.particle_effect(pos, type) if type == 'arrow' then return minetest.add_particlespawner({ amount = 1, time = 0.1, minpos = pos, maxpos = pos, minexptime = 1, maxexptime = 1, minsize = 2, maxsize = 2, texture = 'x_bows_arrow_particle.png', animation = { type = 'vertical_frames', aspect_w = 8, aspect_h = 8, length = 1, }, glow = 1 }) elseif type == 'arrow_crit' then return minetest.add_particlespawner({ amount = 3, time = 0.1, minpos = pos, maxpos = pos, minexptime = 0.5, maxexptime = 0.5, minsize = 2, maxsize = 2, texture = 'x_bows_arrow_particle.png^[colorize:#B22222:127', animation = { type = 'vertical_frames', aspect_w = 8, aspect_h = 8, length = 1, }, glow = 1 }) elseif type == 'arrow_fast' then return minetest.add_particlespawner({ amount = 3, time = 0.1, minpos = pos, maxpos = pos, minexptime = 0.5, maxexptime = 0.5, minsize = 2, maxsize = 2, texture = 'x_bows_arrow_particle.png^[colorize:#0000FF:64', animation = { type = 'vertical_frames', aspect_w = 8, aspect_h = 8, length = 1, }, glow = 1 }) elseif type == 'bubble' then return minetest.add_particlespawner({ amount = 1, time = 1, minpos = pos, maxpos = pos, minvel = {x=1, y=1, z=0}, maxvel = {x=1, y=1, z=0}, minacc = {x=1, y=1, z=1}, maxacc = {x=1, y=1, z=1}, minexptime = 0.2, maxexptime = 0.5, minsize = 0.5, maxsize = 1, texture = 'x_bows_bubble.png' }) elseif type == 'arrow_tipped' then return minetest.add_particlespawner({ amount = 5, time = 1, minpos = vector.subtract(pos, 0.5), maxpos = vector.add(pos, 0.5), minexptime = 0.4, maxexptime = 0.8, minvel = {x=-0.4, y=0.4, z=-0.4}, maxvel = {x=0.4, y=0.6, z=0.4}, minacc = {x=0.2, y=0.4, z=0.2}, maxacc = {x=0.4, y=0.6, z=0.4}, minsize = 4, maxsize = 6, texture = 'x_bows_arrow_tipped_particle.png^[colorize:#008000:127', animation = { type = 'vertical_frames', aspect_w = 8, aspect_h = 8, length = 1, }, glow = 1 }) end end -- sneak, fov adjustments when bow is charged minetest.register_globalstep(function(dtime) bow_charged_timer = bow_charged_timer + dtime if bow_charged_timer > 0.5 then for _, player in ipairs(minetest.get_connected_players()) do local name = player:get_player_name() local stack = player:get_wielded_item() local item = stack:get_name() if not item then return end if not x_bows.player_bow_sneak[name] then x_bows.player_bow_sneak[name] = {} end if item == 'x_bows:bow_wood_charged' and not x_bows.player_bow_sneak[name].sneak then if x_bows.playerphysics then playerphysics.add_physics_factor(player, 'speed', 'x_bows:bow_wood_charged', 0.25) elseif x_bows.player_monoids then player_monoids.speed:add_change(player, 0.25, 'x_bows:bow_wood_charged') end x_bows.player_bow_sneak[name].sneak = true player:set_fov(0.9, true, 0.4) elseif item ~= 'x_bows:bow_wood_charged' and x_bows.player_bow_sneak[name].sneak then if x_bows.playerphysics then playerphysics.remove_physics_factor(player, 'speed', 'x_bows:bow_wood_charged') elseif x_bows.player_monoids then player_monoids.speed:del_change(player, 'x_bows:bow_wood_charged') end x_bows.player_bow_sneak[name].sneak = false player:set_fov(0, true, 0.4) end reset_charged_bow(player) end bow_charged_timer = 0 end end) local path = minetest.get_modpath('x_bows') dofile(path .. '/nodes.lua') dofile(path .. '/arrow.lua') dofile(path .. '/items.lua') dofile(path .. '/quiver.lua') local mod_end_time = (minetest.get_us_time() - mod_start_time) / 1000000 print('[Mod] x_bows loaded.. ['.. mod_end_time ..'s]')