functions.lua 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. -- Function is badly named! Should be 'entity_ignores_arrow'.
  2. -- Return 'true' if the entity cannot be hit, otherwise return 'false' if the entity should be punched.
  3. -- Note: 'entity_name' is the registered name of the entity to be checked for hit.
  4. function throwing.entity_ignores_arrow(entity_name)
  5. -- Dropped itemstacks don't take damage.
  6. if entity_name == "__builtin:item" then
  7. return true
  8. end
  9. -- Ignore other arrows/fireballs in flight.
  10. local is_arrow = (string.find(entity_name, "arrow") or string.find(entity_name, "fireball"))
  11. if is_arrow then
  12. return true
  13. end
  14. -- Ignore health gauges above players.
  15. if entity_name == "gauges:hp_bar" then
  16. return true
  17. end
  18. -- Ignore sound beacons.
  19. if entity_name:find("^soundbeacon:") then
  20. return true
  21. end
  22. -- Entity is unknown, so punch it for damage!
  23. return false
  24. end
  25. --~
  26. --~ Shot and reload system
  27. --~
  28. local players = {}
  29. minetest.register_on_joinplayer(function(player)
  30. local playerName = player:get_player_name()
  31. players[playerName] = {
  32. reloading=false,
  33. }
  34. end)
  35. minetest.register_on_leaveplayer(function(player)
  36. local playerName = player:get_player_name()
  37. players[playerName] = nil
  38. end)
  39. -- This function copied and translated from C++, from one of my C++ projects.
  40. local function rotate_point_2d(p, r)
  41. local x = p.x
  42. local y = p.y
  43. -- Rotate.
  44. local s = math.sin(r)
  45. local c = math.cos(r)
  46. -- Temp vars required to avoid clobbering the equation.
  47. -- This mistake caused me quite a bit of wasted time!
  48. local nx = x*c - y*s
  49. local ny = x*s + y*c
  50. x = nx
  51. y = ny
  52. return {x=x, y=y}
  53. end
  54. local function get_shoot_position(player)
  55. local yaw = player:get_look_horizontal()
  56. local pos = player:get_pos()
  57. local off = {x=0.3, y=0}
  58. --local off = {x=0, y=0}
  59. off = rotate_point_2d(off, yaw)
  60. pos.x = pos.x + off.x
  61. pos.y = pos.y + 1.5
  62. pos.z = pos.z + off.y
  63. return pos
  64. end
  65. -- Can't do this, MT's API is broken here.
  66. -- I posted an issue to the devs: https://github.com/minetest/minetest/issues/14143
  67. local function do_bow_recoil(player)
  68. --[[
  69. local off1 = math.random(-(math.pi*100), (math.pi*100))/100 * 0.02
  70. local off2 = math.random(-(math.pi*100), 0.0)/100 * 0.03
  71. -- set_look_horizontal() is broken.
  72. local oldyaw = player:get_look_horizontal()
  73. local newyaw = oldyaw + off1
  74. player:set_look_horizontal(newyaw)
  75. -- this is also broken.
  76. local oldpitch = player:get_look_vertical()
  77. local newpitch = oldpitch + off2
  78. player:set_look_vertical(newpitch)
  79. --]]
  80. end
  81. function throwing_shoot_arrow(itemstack, player, stiffness, is_cross)
  82. if not player or not player:is_player() then return end
  83. local pname = player:get_player_name()
  84. local arrow = itemstack:get_metadata()
  85. local imeta = itemstack:get_meta()
  86. if arrow == "" then
  87. arrow = imeta:get_string("arrow")
  88. end
  89. if arrow == "" then return end
  90. local playerpos = player:get_pos()
  91. local spawnpos = get_shoot_position(player)
  92. local obj = minetest.add_entity(spawnpos, arrow)
  93. if not obj then return end
  94. local luaent = obj:get_luaentity()
  95. if not luaent then return end
  96. itemstack:set_metadata("")
  97. imeta:set_string("arrow", nil)
  98. imeta:set_string("ar_desc", nil)
  99. toolranks.apply_description(imeta, itemstack:get_definition())
  100. local accuracy = math.rad(math.random(-300, 300)/300)
  101. local dir = player:get_look_dir()
  102. local vel = stiffness * 2
  103. obj:set_velocity({x=dir.x*vel, y=dir.y*vel, z=dir.z*vel})
  104. obj:set_acceleration({x=0, y=-5, z=0})
  105. obj:set_yaw(player:get_look_horizontal() - (math.pi / 2) + accuracy)
  106. if is_cross then
  107. minetest.sound_play("throwing_crossbow_sound", {pos=playerpos}, true)
  108. else
  109. minetest.sound_play("throwing_bow_launch", {pos=playerpos}, true)
  110. end
  111. luaent.player = player
  112. luaent.player_name = pname
  113. luaent.inventory = player:get_inventory()
  114. luaent.stack = player:get_inventory():get_stack("main", player:get_wield_index()-1)
  115. luaent.lastpos = table.copy(spawnpos)
  116. -- Firing anything disables your cloak.
  117. cloaking.disable_if_enabled(pname, true)
  118. do_bow_recoil(player)
  119. -- Return the modified itemstack.
  120. return itemstack
  121. end
  122. function throwing.flight_particle(lpos, cpos)
  123. local mpos = {
  124. x = (lpos.x + cpos.x) / 2,
  125. y = (lpos.y + cpos.y) / 2,
  126. z = (lpos.z + cpos.z) / 2,
  127. }
  128. local mpos2 = {
  129. x = (mpos.x + cpos.x) / 2,
  130. y = (mpos.y + cpos.y) / 2,
  131. z = (mpos.z + cpos.z) / 2,
  132. }
  133. local mpos3 = {
  134. x = (lpos.x + mpos.x) / 2,
  135. y = (lpos.y + mpos.y) / 2,
  136. z = (lpos.z + mpos.z) / 2,
  137. }
  138. local targets = {
  139. mpos3,
  140. mpos,
  141. mpos2,
  142. cpos,
  143. }
  144. for i = 1, #targets, 1 do
  145. local mpos = targets[i]
  146. minetest.add_particlespawner({
  147. amount = 3,
  148. time = 0.1,
  149. minpos = mpos,
  150. maxpos = mpos,
  151. minvel = {x=-0.1, y=-0.1, z=-0.1},
  152. maxvel = {x=0.1, y=0.1, z=0.1},
  153. minacc = vector.new(),
  154. maxacc = vector.new(),
  155. minexptime = 0.5,
  156. maxexptime = 2.5,
  157. minsize = 0.5,
  158. maxsize = 1,
  159. texture = "throwing_sparkle.png",
  160. glow = 8,
  161. })
  162. end
  163. end
  164. local function highlight_position(pos)
  165. utility.original_add_particle({
  166. pos = pos,
  167. velocity = {x=0, y=0, z=0},
  168. acceleration = {x=0, y=0, z=0},
  169. expirationtime = 1.5,
  170. size = 4,
  171. collisiondetection = false,
  172. vertical = false,
  173. texture = "heart.png",
  174. })
  175. end
  176. -- Do flying/collision logic, and execute entity callbacks.
  177. -- This should be called inside of the entity's on_step() function.
  178. -- Returns 'true' if the entity was removed and should no longer be used.
  179. function throwing.do_fly(self, dtime)
  180. -- Get arrow's current and previous position.
  181. local cpos = self.object:get_pos()
  182. local lpos = self.lastpos
  183. ambiance.sound_play("throwing_arrow_fly", cpos, 1.0, 4)
  184. -- Detect collisions: raycast from last position to current. (Note: 'lastpos'
  185. -- table is never nil because it is part of entity definition. This is why
  186. -- test is against 'x' key here.)
  187. --
  188. -- Update: arrow throwing function now always sets 'lastpos' when the arrow
  189. -- entity is spawned (to solve problems where the arrow has moved some distance
  190. -- before the 'on_step' function gets called). Still checking this to avoid
  191. -- problems with old arrow entities in the world.
  192. if lpos.x ~= nil then
  193. local ray = minetest.raycast(lpos, cpos, true, true)
  194. for thing in ray do
  195. if thing.type == "node" then
  196. local nodeu = minetest.get_node(thing.under)
  197. local nodea = minetest.get_node(thing.above)
  198. local blocku = throwing_node_should_block_arrow(nodeu.name)
  199. local blocka = throwing_node_should_block_arrow(nodea.name)
  200. if not blocka and blocku then
  201. -- Test shows that nodeboxes are indeed collisioned correctly.
  202. --if thing.intersection_point then
  203. -- highlight_position(thing.intersection_point)
  204. --end
  205. if self.hit_node then
  206. self:hit_node(thing.under, thing.above, thing.intersection_point)
  207. end
  208. self.object:remove()
  209. return true
  210. elseif (blocka and blocku) or (blocka and not blocku) then
  211. -- Arrow was fired from inside solid nodes.
  212. self.object:remove()
  213. return true
  214. end
  215. elseif thing.type == "object" and thing.ref then
  216. local obj = thing.ref
  217. if obj:is_player() then
  218. -- Not permitted to hit the player that fired it.
  219. if obj:get_player_name() ~= self.player_name then
  220. local continue = false
  221. if self.hit_player then
  222. -- If function returns true, arrow continues flight through this object.
  223. if self:hit_player(obj, thing.intersection_point) then
  224. continue = true
  225. end
  226. end
  227. if not continue then
  228. self.object:remove()
  229. return true
  230. end
  231. end
  232. else
  233. local ent = obj:get_luaentity()
  234. if ent and not throwing.entity_ignores_arrow(ent.name) then
  235. local continue = false
  236. if self.hit_object then
  237. -- If function returns true, arrow continues flight through this object.
  238. if self:hit_object(obj, thing.intersection_point) then
  239. continue = true
  240. end
  241. end
  242. if not continue then
  243. self.object:remove()
  244. return true
  245. end
  246. end
  247. end
  248. end
  249. end
  250. if self.flight_particle then
  251. self:flight_particle(lpos, cpos)
  252. else
  253. throwing.flight_particle(lpos, cpos)
  254. end
  255. end
  256. self.lastpos = {x=cpos.x, y=cpos.y, z=cpos.z}
  257. end
  258. function throwing_unload (itemstack, player, unloaded, wear)
  259. if itemstack:get_metadata() then
  260. for _,arrow in ipairs(throwing_arrows) do
  261. local arw = itemstack:get_metadata()
  262. if arw == "" then
  263. local imeta = itemstack:get_meta()
  264. arw = imeta:get_string("arrow")
  265. end
  266. if arw ~= "" then
  267. if arw == arrow[2] then
  268. local leftover = player:get_inventory():add_item("main", arrow[1])
  269. minetest.item_drop(leftover, player, player:get_pos())
  270. end
  271. end
  272. end
  273. end
  274. if wear >= 65535 then
  275. ambiance.sound_play("default_tool_breaks", player:get_pos(), 1.0, 20)
  276. itemstack:take_item(itemstack:get_count())
  277. return itemstack
  278. else
  279. local newstack = ItemStack(unloaded)
  280. newstack:set_wear(wear)
  281. local imeta = newstack:get_meta()
  282. local ometa = itemstack:get_meta()
  283. imeta:set_string("en_desc", ometa:get_string("en_desc"))
  284. toolranks.apply_description(imeta, newstack:get_definition())
  285. return newstack
  286. end
  287. end
  288. function throwing_arrow_punch_entity (target, self, damage)
  289. -- Get tool capabilities from the tool-data API.
  290. local toolcaps = td_api.arrow_toolcaps(self._name or "", damage)
  291. local player = minetest.get_player_by_name(self.player_name or "")
  292. if player and player:is_player() then
  293. armor.notify_punch_reason({reason="arrow"})
  294. target:punch(player, 1.0, toolcaps, nil)
  295. else
  296. -- Shooter logged off game after firing arrow. Use basic fallback.
  297. toolcaps.damage_groups.from_arrow = nil
  298. armor.notify_punch_reason({reason="arrow"})
  299. target:punch(self.object, 1.0, toolcaps, nil)
  300. end
  301. end
  302. function throwing_reload (index, indexname, controls, pname, pos, is_cross, loaded)
  303. -- This function is called after some delay.
  304. local player = minetest.get_player_by_name(pname)
  305. -- Check for nil. Can happen if player leaves game right after reloading.
  306. if not player or not players[pname] then
  307. return
  308. end
  309. pova.remove_modifier(player, "physics", "bow_reloading")
  310. players[pname].reloading = false
  311. local playerinv = player:get_inventory()
  312. local itemstack = playerinv:get_stack("main", index)
  313. if not itemstack or itemstack:get_count() ~= 1 then
  314. return
  315. end
  316. -- Check if the player is still wielding the same object.
  317. -- This check isn't very secure, but we don't care too much.
  318. local same_selected = false
  319. if index == player:get_wield_index() then
  320. if indexname == itemstack:get_name() then
  321. same_selected = true
  322. end
  323. end
  324. if same_selected then
  325. 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
  326. local wear = itemstack:get_wear()
  327. local bowdef = minetest.registered_items[itemstack:get_name()]
  328. local bowname = bowdef.description
  329. local arrow_index = 1
  330. -- The selected arrow can come from 1 of 4 places to the right of the bow,
  331. -- depending on player's controls at the time the load operation was started.
  332. if controls.sneak and not controls.aux1 then
  333. arrow_index = 2
  334. elseif not controls.sneak and controls.aux1 then
  335. arrow_index = 3
  336. elseif controls.sneak and controls.aux1 then
  337. arrow_index = 4
  338. end
  339. local arrow_stack = playerinv:get_stack("main", index + arrow_index)
  340. for _, arrow in ipairs(throwing_arrows) do
  341. if arrow_stack:get_name() == arrow[1] then
  342. -- Remove arrow from beside bow.
  343. arrow_stack:take_item()
  344. playerinv:set_stack("main", index + arrow_index, arrow_stack)
  345. local name = arrow[1]
  346. local arrowdesc = utility.get_short_desc(minetest.registered_items[name].description or "")
  347. local entity = arrow[2]
  348. -- Replace with loaded bow item.
  349. local newstack = ItemStack(loaded)
  350. newstack:set_wear(wear)
  351. local imeta = newstack:get_meta()
  352. -- Preserve name of bow (if named).
  353. local ometa = itemstack:get_meta()
  354. imeta:set_string("en_desc", ometa:get_string("en_desc"))
  355. imeta:set_string("arrow", entity)
  356. imeta:set_string("ar_desc", arrowdesc)
  357. toolranks.apply_description(imeta, bowdef)
  358. playerinv:set_stack("main", index, newstack)
  359. -- Start checking to see if player unwields this bow. If they do, we
  360. -- must unload it.
  361. throwing.wield_check(pname, index, indexname, loaded)
  362. ambiance.sound_play("throwing_arrow_nock", pos, 1.0, 16)
  363. -- Don't need to iterate through remaining arrow types.
  364. return
  365. end
  366. end
  367. end
  368. end
  369. end
  370. function throwing.wield_check(pname, index, unloaded, loaded)
  371. local player = minetest.get_player_by_name(pname)
  372. if not player then
  373. return
  374. end
  375. local cindex = player:get_wield_index()
  376. if cindex ~= index then
  377. local stack = player:get_inventory():get_stack("main", index)
  378. if stack:get_name() == loaded then
  379. local newstack = throwing_unload(stack, player, unloaded, stack:get_wear())
  380. if newstack then
  381. player:get_inventory():set_stack("main", index, newstack)
  382. end
  383. end
  384. return
  385. end
  386. minetest.after(0, throwing.wield_check, pname, index, unloaded, loaded)
  387. end
  388. -- Bows and crossbows
  389. function throwing_register_bow (name, desc, scale, stiffness, reload_time, toughness, is_cross, craft)
  390. local bow_unloaded_name = "throwing:" .. name
  391. local bow_loaded_name = "throwing:" .. name .. "_loaded"
  392. minetest.register_tool(bow_unloaded_name, {
  393. description = desc,
  394. inventory_image = "throwing_" .. name .. ".png",
  395. wield_scale = scale,
  396. stack_max = 1,
  397. groups = {not_repaired_by_anvil=1},
  398. on_use = function(itemstack, user, pt)
  399. if not user or not user:is_player() then
  400. return
  401. end
  402. local pos = user:get_pos()
  403. local pname = user:get_player_name()
  404. local index = user:get_wield_index()
  405. local inv = user:get_inventory()
  406. local stack = inv:get_stack("main", index)
  407. local indexname = ""
  408. if stack and stack:get_count() == 1 then
  409. indexname = stack:get_name()
  410. end
  411. -- Reload bow after some delay.
  412. if not players[pname].reloading then
  413. pova.set_modifier(user, "physics", {speed=0.7}, "bow_reloading")
  414. players[pname].reloading = true
  415. local controls = user:get_player_control()
  416. minetest.after(reload_time, throwing_reload, index, indexname, controls, pname, pos, is_cross, "throwing:" .. name .. "_loaded")
  417. end
  418. end,
  419. })
  420. minetest.register_tool(bow_loaded_name, {
  421. description = desc,
  422. inventory_image = "throwing_" .. name .. "_loaded.png",
  423. wield_scale = scale,
  424. stack_max = 1,
  425. groups = {not_in_creative_inventory=1, not_repaired_by_anvil=1},
  426. on_use = function(itemstack, user, pt)
  427. if not user or not user:is_player() then
  428. return
  429. end
  430. local control = user:get_player_control()
  431. local unloaded = "throwing:" .. name
  432. local wear = itemstack:get_wear()
  433. -- Unload the bow.
  434. if control.sneak then
  435. local newstack = throwing_unload(itemstack, user, unloaded, wear)
  436. if newstack then
  437. return newstack
  438. end
  439. return itemstack
  440. end
  441. -- Fire the bow.
  442. local newstack = throwing_shoot_arrow(itemstack, user, stiffness, is_cross)
  443. if newstack then
  444. wear = wear + (65535 / toughness)
  445. newstack = throwing_unload(newstack, user, unloaded, wear)
  446. end
  447. if newstack then
  448. return newstack
  449. end
  450. return itemstack
  451. end,
  452. -- Prevent dropping loaded bows.
  453. on_drop = function(itemstack, dropper, pos) return itemstack end,
  454. })
  455. -- Store loaded/unloaded bow names.
  456. throwing.bow_names_loaded[#(throwing.bow_names_loaded) + 1] = {
  457. name = bow_loaded_name,
  458. unloaded = bow_unloaded_name,
  459. }
  460. throwing.bow_names_unloaded[#(throwing.bow_names_unloaded) + 1] = {
  461. name = bow_unloaded_name,
  462. loaded = bow_loaded_name,
  463. }
  464. minetest.register_craft({
  465. output = 'throwing:' .. name,
  466. recipe = craft
  467. })
  468. minetest.register_craft({
  469. output = 'throwing:' .. name,
  470. recipe = {
  471. {craft[1][3], craft[1][2], craft[1][1]},
  472. {craft[2][3], craft[2][2], craft[2][1]},
  473. {craft[3][3], craft[3][2], craft[3][1]},
  474. }
  475. })
  476. end
  477. -- Determine if a node should block an arrow.
  478. -- Cheapest checks should come first.
  479. function throwing_node_should_block_arrow (nn)
  480. if nn == "air" then return false end
  481. if snow.is_snow(nn) then return false end
  482. --if nn == "ignore" then return true end
  483. if string.find(nn, "^throwing:") or
  484. string.find(nn, "^fire:") or
  485. string.find(nn, "^default:fence") or
  486. string.find(nn, "ladder") then
  487. return false
  488. end
  489. local def = minetest.reg_ns_nodes[nn]
  490. if def then
  491. local dt = def.drawtype
  492. local pt2 = def.paramtype2
  493. if dt == "airlike" or
  494. dt == "signlike" or
  495. dt == "torchlike" or
  496. dt == "raillike" or
  497. dt == "plantlike" or
  498. (dt == "nodebox" and pt2 == "wallmounted") then
  499. return false
  500. end
  501. end
  502. return true
  503. end
  504. throwing.node_blocks_arrow = throwing_node_should_block_arrow
  505. -- Prevent moving loaded bows out of player inventory to other inventories.
  506. -- The action has to be prevented completely (we cannot simply unload the bow)
  507. -- because we cannot track the itemstack after it has been moved out of the
  508. -- player's inventory.
  509. local function inventory_allow(player, action, inventory, inventory_info)
  510. if action == "take" then
  511. local sname = inventory_info.stack:get_name()
  512. if sname:find("^throwing:") then
  513. for k, v in ipairs(throwing.bow_names_loaded) do
  514. if v.name == sname then
  515. return 0
  516. end
  517. end
  518. end
  519. end
  520. end
  521. local function inventory_action(player, action, inventory, inventory_info)
  522. if action == "put" then
  523. local sname = inventory_info.stack:get_name()
  524. if sname:find("^throwing:") then
  525. for k, v in ipairs(throwing.bow_names_loaded) do
  526. if v.name == sname then
  527. -- Player has put a loaded bow into their inventory from somewhere else.
  528. -- Unload the bow to prevent a possible exploit.
  529. local bowstack = inventory_info.stack
  530. local newstack = throwing_unload(bowstack, player, v.unloaded, bowstack:get_wear())
  531. if newstack then
  532. player:get_inventory():set_stack(inventory_info.listname, inventory_info.index, newstack)
  533. end
  534. end
  535. end
  536. end
  537. elseif action == "move" then
  538. local movedstack = player:get_inventory():get_stack(inventory_info.to_list, inventory_info.to_index)
  539. local sname = movedstack:get_name()
  540. if sname:find("^throwing:") then
  541. for k, v in ipairs(throwing.bow_names_loaded) do
  542. if v.name == sname then
  543. -- Player has moved a bow around in their inventory.
  544. -- Unloaded it, as they almost certainly aren't wielding it anymore.
  545. local bowstack = movedstack
  546. local newstack = throwing_unload(bowstack, player, v.unloaded, bowstack:get_wear())
  547. if newstack then
  548. player:get_inventory():set_stack(inventory_info.to_list, inventory_info.to_index, newstack)
  549. end
  550. end
  551. end
  552. end
  553. end
  554. end
  555. local function get_unloaded_name(loaded)
  556. for k, v in ipairs(throwing.bow_names_loaded) do
  557. if v.name == loaded then
  558. return v.unloaded
  559. end
  560. end
  561. end
  562. local function unload_all_bows(player)
  563. local inv = player:get_inventory()
  564. local sz = inv:get_size("main")
  565. for k = 1, sz do
  566. local stack = inv:get_stack("main", k)
  567. local sname = stack:get_name()
  568. if sname:find("^throwing:") and sname:find("_loaded$") then
  569. local unloaded = get_unloaded_name(sname)
  570. if unloaded then
  571. local newstack = throwing_unload(stack, player, unloaded, stack:get_wear())
  572. if newstack then
  573. inv:set_stack("main", k, newstack)
  574. end
  575. end
  576. end
  577. end
  578. end
  579. minetest.register_allow_player_inventory_action(inventory_allow)
  580. minetest.register_on_player_inventory_action(inventory_action)
  581. minetest.register_on_joinplayer(unload_all_bows)