123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704 |
- -- Function is badly named! Should be 'entity_ignores_arrow'.
- -- Return 'true' if the entity cannot be hit, otherwise return 'false' if the entity should be punched.
- -- Note: 'entity_name' is the registered name of the entity to be checked for hit.
- function throwing.entity_ignores_arrow(entity_name)
- -- Dropped itemstacks don't take damage.
- if entity_name == "__builtin:item" then
- return true
- end
- -- Ignore other arrows/fireballs in flight.
- local is_arrow = (string.find(entity_name, "arrow") or string.find(entity_name, "fireball"))
- if is_arrow then
- return true
- end
- -- Ignore health gauges above players.
- if entity_name == "gauges:hp_bar" then
- return true
- end
- -- Ignore sound beacons.
- if entity_name:find("^soundbeacon:") then
- return true
- end
- -- Entity is unknown, so punch it for damage!
- return false
- end
- --~
- --~ Shot and reload system
- --~
- local players = {}
- minetest.register_on_joinplayer(function(player)
- local playerName = player:get_player_name()
- players[playerName] = {
- reloading=false,
- }
- end)
- minetest.register_on_leaveplayer(function(player)
- local playerName = player:get_player_name()
- players[playerName] = nil
- end)
- -- This function copied and translated from C++, from one of my C++ projects.
- local function rotate_point_2d(p, r)
- local x = p.x
- local y = p.y
- -- Rotate.
- local s = math.sin(r)
- local c = math.cos(r)
- -- Temp vars required to avoid clobbering the equation.
- -- This mistake caused me quite a bit of wasted time!
- local nx = x*c - y*s
- local ny = x*s + y*c
- x = nx
- y = ny
- return {x=x, y=y}
- end
- local function get_shoot_position(player)
- local yaw = player:get_look_horizontal()
- local pos = player:get_pos()
- local off = {x=0.3, y=0}
- --local off = {x=0, y=0}
- off = rotate_point_2d(off, yaw)
- pos.x = pos.x + off.x
- pos.y = pos.y + 1.5
- pos.z = pos.z + off.y
- return pos
- end
- -- Can't do this, MT's API is broken here.
- -- I posted an issue to the devs: https://github.com/minetest/minetest/issues/14143
- local function do_bow_recoil(player)
- --[[
- local off1 = math.random(-(math.pi*100), (math.pi*100))/100 * 0.02
- local off2 = math.random(-(math.pi*100), 0.0)/100 * 0.03
- -- set_look_horizontal() is broken.
- local oldyaw = player:get_look_horizontal()
- local newyaw = oldyaw + off1
- player:set_look_horizontal(newyaw)
- -- this is also broken.
- local oldpitch = player:get_look_vertical()
- local newpitch = oldpitch + off2
- player:set_look_vertical(newpitch)
- --]]
- end
- function throwing_shoot_arrow(itemstack, player, stiffness, is_cross)
- if not player or not player:is_player() then return end
- local pname = player:get_player_name()
-
- local arrow = itemstack:get_metadata()
- local imeta = itemstack:get_meta()
- if arrow == "" then
- arrow = imeta:get_string("arrow")
- end
- if arrow == "" then return end
-
- local playerpos = player:get_pos()
- local spawnpos = get_shoot_position(player)
- local obj = minetest.add_entity(spawnpos, arrow)
- if not obj then return end
- local luaent = obj:get_luaentity()
- if not luaent then return end
- itemstack:set_metadata("")
- imeta:set_string("arrow", nil)
- imeta:set_string("ar_desc", nil)
- toolranks.apply_description(imeta, itemstack:get_definition())
- local accuracy = math.rad(math.random(-300, 300)/300)
- local dir = player:get_look_dir()
- local vel = stiffness * 2
- obj:set_velocity({x=dir.x*vel, y=dir.y*vel, z=dir.z*vel})
- obj:set_acceleration({x=0, y=-5, z=0})
- obj:set_yaw(player:get_look_horizontal() - (math.pi / 2) + accuracy)
- if is_cross then
- minetest.sound_play("throwing_crossbow_sound", {pos=playerpos}, true)
- else
- minetest.sound_play("throwing_bow_launch", {pos=playerpos}, true)
- end
- luaent.player = player
- luaent.player_name = pname
- luaent.inventory = player:get_inventory()
- luaent.stack = player:get_inventory():get_stack("main", player:get_wield_index()-1)
- luaent.lastpos = table.copy(spawnpos)
- -- Firing anything disables your cloak.
- cloaking.disable_if_enabled(pname, true)
- do_bow_recoil(player)
- -- Return the modified itemstack.
- return itemstack
- end
- function throwing.flight_particle(lpos, cpos)
- local mpos = {
- x = (lpos.x + cpos.x) / 2,
- y = (lpos.y + cpos.y) / 2,
- z = (lpos.z + cpos.z) / 2,
- }
- local mpos2 = {
- x = (mpos.x + cpos.x) / 2,
- y = (mpos.y + cpos.y) / 2,
- z = (mpos.z + cpos.z) / 2,
- }
- local mpos3 = {
- x = (lpos.x + mpos.x) / 2,
- y = (lpos.y + mpos.y) / 2,
- z = (lpos.z + mpos.z) / 2,
- }
- local targets = {
- mpos3,
- mpos,
- mpos2,
- cpos,
- }
- for i = 1, #targets, 1 do
- local mpos = targets[i]
- minetest.add_particlespawner({
- amount = 3,
- time = 0.1,
- minpos = mpos,
- maxpos = mpos,
- minvel = {x=-0.1, y=-0.1, z=-0.1},
- maxvel = {x=0.1, y=0.1, z=0.1},
- minacc = vector.new(),
- maxacc = vector.new(),
- minexptime = 0.5,
- maxexptime = 2.5,
- minsize = 0.5,
- maxsize = 1,
- texture = "throwing_sparkle.png",
- glow = 8,
- })
- end
- end
- local function highlight_position(pos)
- utility.original_add_particle({
- pos = pos,
- velocity = {x=0, y=0, z=0},
- acceleration = {x=0, y=0, z=0},
- expirationtime = 1.5,
- size = 4,
- collisiondetection = false,
- vertical = false,
- texture = "heart.png",
- })
- end
- -- Do flying/collision logic, and execute entity callbacks.
- -- This should be called inside of the entity's on_step() function.
- -- Returns 'true' if the entity was removed and should no longer be used.
- function throwing.do_fly(self, dtime)
- -- Get arrow's current and previous position.
- local cpos = self.object:get_pos()
- local lpos = self.lastpos
- ambiance.sound_play("throwing_arrow_fly", cpos, 1.0, 4)
- -- Detect collisions: raycast from last position to current. (Note: 'lastpos'
- -- table is never nil because it is part of entity definition. This is why
- -- test is against 'x' key here.)
- --
- -- Update: arrow throwing function now always sets 'lastpos' when the arrow
- -- entity is spawned (to solve problems where the arrow has moved some distance
- -- before the 'on_step' function gets called). Still checking this to avoid
- -- problems with old arrow entities in the world.
- if lpos.x ~= nil then
- local ray = minetest.raycast(lpos, cpos, true, true)
- for thing in ray do
- if thing.type == "node" then
- local nodeu = minetest.get_node(thing.under)
- local nodea = minetest.get_node(thing.above)
- local blocku = throwing_node_should_block_arrow(nodeu.name)
- local blocka = throwing_node_should_block_arrow(nodea.name)
- if not blocka and blocku then
- -- Test shows that nodeboxes are indeed collisioned correctly.
- --if thing.intersection_point then
- -- highlight_position(thing.intersection_point)
- --end
- if self.hit_node then
- self:hit_node(thing.under, thing.above, thing.intersection_point)
- end
- self.object:remove()
- return true
- elseif (blocka and blocku) or (blocka and not blocku) then
- -- Arrow was fired from inside solid nodes.
- self.object:remove()
- return true
- end
- elseif thing.type == "object" and thing.ref then
- local obj = thing.ref
- if obj:is_player() then
- -- Not permitted to hit the player that fired it.
- if obj:get_player_name() ~= self.player_name then
- local continue = false
- if self.hit_player then
- -- If function returns true, arrow continues flight through this object.
- if self:hit_player(obj, thing.intersection_point) then
- continue = true
- end
- end
- if not continue then
- self.object:remove()
- return true
- end
- end
- else
- local ent = obj:get_luaentity()
- if ent and not throwing.entity_ignores_arrow(ent.name) then
- local continue = false
- if self.hit_object then
- -- If function returns true, arrow continues flight through this object.
- if self:hit_object(obj, thing.intersection_point) then
- continue = true
- end
- end
- if not continue then
- self.object:remove()
- return true
- end
- end
- end
- end
- end
- if self.flight_particle then
- self:flight_particle(lpos, cpos)
- else
- throwing.flight_particle(lpos, cpos)
- end
- end
- self.lastpos = {x=cpos.x, y=cpos.y, z=cpos.z}
- end
- function throwing_unload (itemstack, player, unloaded, wear)
- if itemstack:get_metadata() then
- for _,arrow in ipairs(throwing_arrows) do
- local arw = itemstack:get_metadata()
- if arw == "" then
- local imeta = itemstack:get_meta()
- arw = imeta:get_string("arrow")
- end
- if arw ~= "" then
- if arw == arrow[2] then
- local leftover = player:get_inventory():add_item("main", arrow[1])
- minetest.item_drop(leftover, player, player:get_pos())
- end
- end
- end
- end
- if wear >= 65535 then
- ambiance.sound_play("default_tool_breaks", player:get_pos(), 1.0, 20)
- itemstack:take_item(itemstack:get_count())
- return itemstack
- else
- local newstack = ItemStack(unloaded)
- newstack:set_wear(wear)
- local imeta = newstack:get_meta()
- local ometa = itemstack:get_meta()
- imeta:set_string("en_desc", ometa:get_string("en_desc"))
- toolranks.apply_description(imeta, newstack:get_definition())
- return newstack
- end
- end
- function throwing_arrow_punch_entity (target, self, damage)
- -- Get tool capabilities from the tool-data API.
- local toolcaps = td_api.arrow_toolcaps(self._name or "", damage)
- local player = minetest.get_player_by_name(self.player_name or "")
- if player and player:is_player() then
- armor.notify_punch_reason({reason="arrow"})
- target:punch(player, 1.0, toolcaps, nil)
- else
- -- Shooter logged off game after firing arrow. Use basic fallback.
- toolcaps.damage_groups.from_arrow = nil
- armor.notify_punch_reason({reason="arrow"})
- target:punch(self.object, 1.0, toolcaps, nil)
- end
- end
- function throwing_reload (index, indexname, controls, pname, pos, is_cross, loaded)
- -- This function is called after some delay.
- local player = minetest.get_player_by_name(pname)
- -- Check for nil. Can happen if player leaves game right after reloading.
- if not player or not players[pname] then
- return
- end
- pova.remove_modifier(player, "physics", "bow_reloading")
- players[pname].reloading = false
- local playerinv = player:get_inventory()
- local itemstack = playerinv:get_stack("main", index)
- if not itemstack or itemstack:get_count() ~= 1 then
- return
- end
- -- Check if the player is still wielding the same object.
- -- This check isn't very secure, but we don't care too much.
- local same_selected = false
- if index == player:get_wield_index() then
- if indexname == itemstack:get_name() then
- same_selected = true
- end
- end
- if same_selected then
- if (pos.x == player:get_pos().x and pos.y == player:get_pos().y and pos.z == player:get_pos().z) or not is_cross then
- local wear = itemstack:get_wear()
- local bowdef = minetest.registered_items[itemstack:get_name()]
- local bowname = bowdef.description
- local arrow_index = 1
- -- The selected arrow can come from 1 of 4 places to the right of the bow,
- -- depending on player's controls at the time the load operation was started.
- if controls.sneak and not controls.aux1 then
- arrow_index = 2
- elseif not controls.sneak and controls.aux1 then
- arrow_index = 3
- elseif controls.sneak and controls.aux1 then
- arrow_index = 4
- end
- local arrow_stack = playerinv:get_stack("main", index + arrow_index)
- for _, arrow in ipairs(throwing_arrows) do
- if arrow_stack:get_name() == arrow[1] then
- -- Remove arrow from beside bow.
- arrow_stack:take_item()
- playerinv:set_stack("main", index + arrow_index, arrow_stack)
- local name = arrow[1]
- local arrowdesc = utility.get_short_desc(minetest.registered_items[name].description or "")
- local entity = arrow[2]
- -- Replace with loaded bow item.
- local newstack = ItemStack(loaded)
- newstack:set_wear(wear)
- local imeta = newstack:get_meta()
- -- Preserve name of bow (if named).
- local ometa = itemstack:get_meta()
- imeta:set_string("en_desc", ometa:get_string("en_desc"))
- imeta:set_string("arrow", entity)
- imeta:set_string("ar_desc", arrowdesc)
- toolranks.apply_description(imeta, bowdef)
- playerinv:set_stack("main", index, newstack)
- -- Start checking to see if player unwields this bow. If they do, we
- -- must unload it.
- throwing.wield_check(pname, index, indexname, loaded)
- ambiance.sound_play("throwing_arrow_nock", pos, 1.0, 16)
- -- Don't need to iterate through remaining arrow types.
- return
- end
- end
- end
- end
- end
- function throwing.wield_check(pname, index, unloaded, loaded)
- local player = minetest.get_player_by_name(pname)
- if not player then
- return
- end
- local cindex = player:get_wield_index()
- if cindex ~= index then
- local stack = player:get_inventory():get_stack("main", index)
- if stack:get_name() == loaded then
- local newstack = throwing_unload(stack, player, unloaded, stack:get_wear())
- if newstack then
- player:get_inventory():set_stack("main", index, newstack)
- end
- end
- return
- end
- minetest.after(0, throwing.wield_check, pname, index, unloaded, loaded)
- end
- -- Bows and crossbows
- function throwing_register_bow (name, desc, scale, stiffness, reload_time, toughness, is_cross, craft)
- local bow_unloaded_name = "throwing:" .. name
- local bow_loaded_name = "throwing:" .. name .. "_loaded"
- minetest.register_tool(bow_unloaded_name, {
- description = desc,
- inventory_image = "throwing_" .. name .. ".png",
- wield_scale = scale,
- stack_max = 1,
- groups = {not_repaired_by_anvil=1},
- on_use = function(itemstack, user, pt)
- if not user or not user:is_player() then
- return
- end
- local pos = user:get_pos()
- local pname = user:get_player_name()
- local index = user:get_wield_index()
- local inv = user:get_inventory()
- local stack = inv:get_stack("main", index)
- local indexname = ""
- if stack and stack:get_count() == 1 then
- indexname = stack:get_name()
- end
- -- Reload bow after some delay.
- if not players[pname].reloading then
- pova.set_modifier(user, "physics", {speed=0.7}, "bow_reloading")
- players[pname].reloading = true
- local controls = user:get_player_control()
- minetest.after(reload_time, throwing_reload, index, indexname, controls, pname, pos, is_cross, "throwing:" .. name .. "_loaded")
- end
- end,
- })
-
- minetest.register_tool(bow_loaded_name, {
- description = desc,
- inventory_image = "throwing_" .. name .. "_loaded.png",
- wield_scale = scale,
- stack_max = 1,
- groups = {not_in_creative_inventory=1, not_repaired_by_anvil=1},
- on_use = function(itemstack, user, pt)
- if not user or not user:is_player() then
- return
- end
- local control = user:get_player_control()
- local unloaded = "throwing:" .. name
- local wear = itemstack:get_wear()
- -- Unload the bow.
- if control.sneak then
- local newstack = throwing_unload(itemstack, user, unloaded, wear)
- if newstack then
- return newstack
- end
- return itemstack
- end
- -- Fire the bow.
- local newstack = throwing_shoot_arrow(itemstack, user, stiffness, is_cross)
- if newstack then
- wear = wear + (65535 / toughness)
- newstack = throwing_unload(newstack, user, unloaded, wear)
- end
- if newstack then
- return newstack
- end
- return itemstack
- end,
- -- Prevent dropping loaded bows.
- on_drop = function(itemstack, dropper, pos) return itemstack end,
- })
- -- Store loaded/unloaded bow names.
- throwing.bow_names_loaded[#(throwing.bow_names_loaded) + 1] = {
- name = bow_loaded_name,
- unloaded = bow_unloaded_name,
- }
- throwing.bow_names_unloaded[#(throwing.bow_names_unloaded) + 1] = {
- name = bow_unloaded_name,
- loaded = bow_loaded_name,
- }
-
- minetest.register_craft({
- output = 'throwing:' .. name,
- recipe = craft
- })
- minetest.register_craft({
- output = 'throwing:' .. name,
- recipe = {
- {craft[1][3], craft[1][2], craft[1][1]},
- {craft[2][3], craft[2][2], craft[2][1]},
- {craft[3][3], craft[3][2], craft[3][1]},
- }
- })
- end
- -- Determine if a node should block an arrow.
- -- Cheapest checks should come first.
- function throwing_node_should_block_arrow (nn)
- if nn == "air" then return false end
- if snow.is_snow(nn) then return false end
- --if nn == "ignore" then return true end
-
- if string.find(nn, "^throwing:") or
- string.find(nn, "^fire:") or
- string.find(nn, "^default:fence") or
- string.find(nn, "ladder") then
- return false
- end
-
- local def = minetest.reg_ns_nodes[nn]
- if def then
- local dt = def.drawtype
- local pt2 = def.paramtype2
- if dt == "airlike" or
- dt == "signlike" or
- dt == "torchlike" or
- dt == "raillike" or
- dt == "plantlike" or
- (dt == "nodebox" and pt2 == "wallmounted") then
- return false
- end
- end
-
- return true
- end
- throwing.node_blocks_arrow = throwing_node_should_block_arrow
- -- Prevent moving loaded bows out of player inventory to other inventories.
- -- The action has to be prevented completely (we cannot simply unload the bow)
- -- because we cannot track the itemstack after it has been moved out of the
- -- player's inventory.
- local function inventory_allow(player, action, inventory, inventory_info)
- if action == "take" then
- local sname = inventory_info.stack:get_name()
- if sname:find("^throwing:") then
- for k, v in ipairs(throwing.bow_names_loaded) do
- if v.name == sname then
- return 0
- end
- end
- end
- end
- end
- local function inventory_action(player, action, inventory, inventory_info)
- if action == "put" then
- local sname = inventory_info.stack:get_name()
- if sname:find("^throwing:") then
- for k, v in ipairs(throwing.bow_names_loaded) do
- if v.name == sname then
- -- Player has put a loaded bow into their inventory from somewhere else.
- -- Unload the bow to prevent a possible exploit.
- local bowstack = inventory_info.stack
- local newstack = throwing_unload(bowstack, player, v.unloaded, bowstack:get_wear())
- if newstack then
- player:get_inventory():set_stack(inventory_info.listname, inventory_info.index, newstack)
- end
- end
- end
- end
- elseif action == "move" then
- local movedstack = player:get_inventory():get_stack(inventory_info.to_list, inventory_info.to_index)
- local sname = movedstack:get_name()
- if sname:find("^throwing:") then
- for k, v in ipairs(throwing.bow_names_loaded) do
- if v.name == sname then
- -- Player has moved a bow around in their inventory.
- -- Unloaded it, as they almost certainly aren't wielding it anymore.
- local bowstack = movedstack
- local newstack = throwing_unload(bowstack, player, v.unloaded, bowstack:get_wear())
- if newstack then
- player:get_inventory():set_stack(inventory_info.to_list, inventory_info.to_index, newstack)
- end
- end
- end
- end
- end
- end
- local function get_unloaded_name(loaded)
- for k, v in ipairs(throwing.bow_names_loaded) do
- if v.name == loaded then
- return v.unloaded
- end
- end
- end
- local function unload_all_bows(player)
- local inv = player:get_inventory()
- local sz = inv:get_size("main")
- for k = 1, sz do
- local stack = inv:get_stack("main", k)
- local sname = stack:get_name()
- if sname:find("^throwing:") and sname:find("_loaded$") then
- local unloaded = get_unloaded_name(sname)
- if unloaded then
- local newstack = throwing_unload(stack, player, unloaded, stack:get_wear())
- if newstack then
- inv:set_stack("main", k, newstack)
- end
- end
- end
- end
- end
- minetest.register_allow_player_inventory_action(inventory_allow)
- minetest.register_on_player_inventory_action(inventory_action)
- minetest.register_on_joinplayer(unload_all_bows)
|