init.lua 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. -- compass configuration interface - adjustable from other mods or minetest.conf settings
  2. death_compass = {}
  3. local S = minetest.get_translator("death_compass")
  4. -- how many seconds does the death compass work for? 0 for indefinite
  5. local duration = tonumber(minetest.settings:get("death_compass_duration")) or 0
  6. local automatic = minetest.settings:get_bool("death_compass_automatic", false)
  7. local range_to_inactivate = 5
  8. local hud_position = {
  9. x= tonumber(minetest.settings:get("death_compass_hud_x")) or 0.5,
  10. y= tonumber(minetest.settings:get("death_compass_hud_y")) or 0.9,
  11. }
  12. local hud_color = tonumber("0x" .. (minetest.settings:get("death_compass_hud_color") or "FFFF00")) or 0xFFFF00
  13. -- If round is true the return string will only have the two largest-scale values
  14. local function clock_string(seconds, round)
  15. seconds = math.floor(seconds)
  16. local days = math.floor(seconds/86400)
  17. seconds = seconds - days*86400
  18. local hours = math.floor(seconds/3600)
  19. seconds = seconds - hours*3600
  20. local minutes = math.floor(seconds/60)
  21. seconds = seconds - minutes*60
  22. local ret = {}
  23. if days == 1 then
  24. table.insert(ret, S("1 day"))
  25. elseif days > 1 then
  26. table.insert(ret, S("@1 days", days))
  27. end
  28. if hours == 1 then
  29. table.insert(ret, S("1 hour"))
  30. elseif hours > 1 then
  31. table.insert(ret, S("@1 hours", hours))
  32. end
  33. if minutes == 1 then
  34. table.insert(ret, S("1 minute"))
  35. elseif minutes > 1 then
  36. table.insert(ret, S("@1 minutes", minutes))
  37. end
  38. if seconds == 1 then
  39. table.insert(ret, S("1 second"))
  40. elseif seconds > 1 then
  41. table.insert(ret, S("@1 seconds", seconds))
  42. end
  43. if #ret == 0 then
  44. return S("@1 seconds", 0)
  45. end
  46. if #ret == 1 then
  47. return ret[1]
  48. end
  49. if round or #ret == 2 then
  50. return S("@1 and @2", ret[1], ret[2])
  51. end
  52. return table.concat(ret, S(", "))
  53. end
  54. local documentation = S("This does nothing in its current inert state. If you have this in your inventory when you die, however, it will follow you into your next life's inventory and point toward the location of your previous life's end.")
  55. local durationdesc
  56. if duration > 0 then
  57. durationdesc = S("The Death Compass' guidance will only last for @1 after death.", clock_string(duration, false))
  58. else
  59. durationdesc = S("The Death Compass will point toward your corpse until you find it.")
  60. end
  61. -- set a position to the compass stack
  62. local function set_target(stack, pos, name)
  63. local meta=stack:get_meta()
  64. meta:set_string("target_pos", minetest.pos_to_string(pos))
  65. meta:set_string("target_corpse", name)
  66. meta:set_int("time_of_death", minetest.get_gametime())
  67. end
  68. -- Get compass target
  69. local function get_destination(player, stack)
  70. local posstring = stack:get_meta():get_string("target_pos")
  71. if posstring ~= "" then
  72. return minetest.string_to_pos(posstring)
  73. end
  74. end
  75. -- looped ticking sound if there's a duration on this
  76. local player_ticking = {}
  77. local function start_ticking(player_name)
  78. if not player_ticking[player_name] then
  79. player_ticking[player_name] = minetest.sound_play("death_compass_tick_tock",
  80. {to_player = player_name, gain = 0.125, loop = true})
  81. end
  82. end
  83. local function stop_ticking(player_name)
  84. local tick_tock_handle = player_ticking[player_name]
  85. if tick_tock_handle then
  86. minetest.sound_stop(tick_tock_handle)
  87. player_ticking[player_name] = nil
  88. end
  89. end
  90. local player_huds = {}
  91. local function hide_hud(player, player_name)
  92. local id = player_huds[player_name]
  93. if id then
  94. player:hud_remove(id)
  95. player_huds[player_name] = nil
  96. end
  97. end
  98. local function update_hud(player, player_name, compass)
  99. local metadata = compass:get_meta()
  100. local target_pos = minetest.string_to_pos(metadata:get_string("target_pos"))
  101. local player_pos = player:get_pos()
  102. local distance = vector.distance(player_pos, target_pos)
  103. if not target_pos then
  104. return
  105. end
  106. local time_of_death = metadata:get_int("time_of_death")
  107. local target_name = metadata:get_string("target_corpse")
  108. local description
  109. if duration > 0 then
  110. local remaining = time_of_death + duration - minetest.get_gametime()
  111. if remaining < 0 then
  112. return
  113. end
  114. description = S("@1m to @2's corpse, @3 remaining", math.floor(distance),
  115. target_name, clock_string(remaining, true))
  116. else
  117. description = S("@1m to @2's corpse, died @3 ago", math.floor(distance),
  118. target_name, clock_string(minetest.get_gametime() - time_of_death, true))
  119. end
  120. local id = player_huds[player_name]
  121. if not id then
  122. id = player:hud_add({
  123. hud_elem_type = "text",
  124. position = hud_position,
  125. text = description,
  126. number = hud_color,
  127. scale = 20,
  128. })
  129. player_huds[player_name] = id
  130. else
  131. player:hud_change(id, "text", description)
  132. end
  133. end
  134. -- get right image number for players compass
  135. local function get_compass_stack(player, stack)
  136. local target = get_destination(player, stack)
  137. local inactive_return
  138. if automatic then
  139. inactive_return = ItemStack("")
  140. else
  141. inactive_return = ItemStack("death_compass:inactive")
  142. end
  143. if not target then
  144. return inactive_return
  145. end
  146. local pos = player:get_pos()
  147. local distance = vector.distance(pos, target)
  148. local player_name = player:get_player_name()
  149. if distance < range_to_inactivate then
  150. stop_ticking(player_name)
  151. minetest.sound_play("death_compass_bone_crunch", {to_player=player_name, gain = 1.0})
  152. return inactive_return
  153. end
  154. local dir = player:get_look_horizontal()
  155. local angle_north = math.deg(math.atan2(target.x - pos.x, target.z - pos.z))
  156. if angle_north < 0 then
  157. angle_north = angle_north + 360
  158. end
  159. local angle_dir = math.deg(dir)
  160. local angle_relative = (angle_north + angle_dir) % 360
  161. local compass_image = math.floor((angle_relative/22.5) + 0.5)%16
  162. -- create new stack with metadata copied
  163. local metadata = stack:get_meta():to_table()
  164. local meta_fields = metadata.fields
  165. local time_of_death = tonumber(meta_fields.time_of_death)
  166. if duration > 0 then
  167. local remaining = time_of_death + duration - minetest.get_gametime()
  168. if remaining < 0 then
  169. stop_ticking(player_name)
  170. minetest.sound_play("death_compass_bone_crunch", {to_player=player_name, gain = 1.0})
  171. return inactive_return
  172. end
  173. start_ticking(player_name)
  174. end
  175. local newstack = ItemStack("death_compass:dir"..compass_image)
  176. if metadata then
  177. newstack:get_meta():from_table(metadata)
  178. end
  179. return newstack
  180. end
  181. -- update inventory and hud
  182. minetest.register_globalstep(function(dtime)
  183. for i, player in ipairs(minetest.get_connected_players()) do
  184. local player_name = player:get_player_name()
  185. local compass_in_quickbar
  186. local inv = player:get_inventory()
  187. if inv then
  188. for i, stack in ipairs(inv:get_list("main")) do
  189. if i > 8 then
  190. break
  191. end
  192. if string.sub(stack:get_name(), 0, 17) == "death_compass:dir" then
  193. player:get_inventory():set_stack("main", i, get_compass_stack(player, stack))
  194. compass_in_quickbar = true
  195. end
  196. end
  197. if compass_in_quickbar then
  198. local wielded = player:get_wielded_item()
  199. if string.sub(wielded:get_name(), 0, 17) == "death_compass:dir" then
  200. update_hud(player, player_name, wielded)
  201. else
  202. hide_hud(player, player_name)
  203. end
  204. end
  205. end
  206. if not compass_in_quickbar then
  207. stop_ticking(player_name)
  208. hide_hud(player, player_name)
  209. end
  210. end
  211. end)
  212. -- register items
  213. for i = 0, 15 do
  214. local image = "death_compass_16_"..i..".png"
  215. minetest.register_craftitem("death_compass:dir"..i, {
  216. description = S("Death Compass"),
  217. inventory_image = image,
  218. wield_image = image,
  219. stack_max = 1,
  220. groups = {death_compass = 1, not_in_creative_inventory = 1},
  221. })
  222. end
  223. if not automatic then
  224. local display_doc = function(itemstack, user)
  225. local player_name = user:get_player_name()
  226. minetest.chat_send_player(player_name, documentation .. "\n" .. durationdesc)
  227. end
  228. minetest.register_craftitem("death_compass:inactive", {
  229. description = S("Death Compass"),
  230. _doc_items_longdesc = documentation,
  231. _doc_items_usagehelp = durationdesc,
  232. inventory_image = "death_compass_inactive.png",
  233. wield_image = "death_compass_inactive.png",
  234. stack_max = 1,
  235. on_place = display_doc,
  236. on_secondary_use = display_doc,
  237. })
  238. minetest.register_craft({
  239. output = 'death_compass:inactive',
  240. recipe = {
  241. {'', 'bones:bones', ''},
  242. {'bones:bones', 'default:mese_crystal_fragment', 'bones:bones'},
  243. {'', 'bones:bones', ''}
  244. }
  245. })
  246. -- Allow a player to deliberately deactivate a death compass
  247. minetest.register_craft({
  248. output = 'death_compass:inactive',
  249. type = "shapeless",
  250. recipe = {
  251. 'group:death_compass',
  252. }
  253. })
  254. end
  255. local player_death_location = {}
  256. minetest.register_on_dieplayer(function(player, reason)
  257. local player_name = player:get_player_name()
  258. local inv = minetest.get_inventory({type="player", name=player:get_player_name()})
  259. local list = inv:get_list("main")
  260. local count = 0
  261. if automatic then
  262. count = 1
  263. else
  264. for i, itemstack in pairs(list) do
  265. if itemstack:get_name() == "death_compass:inactive" then
  266. count = count + itemstack:get_count()
  267. list[i] = ItemStack("")
  268. end
  269. end
  270. end
  271. if count > 0 then
  272. inv:set_list("main", list)
  273. player_death_location[player_name] = {count=count,pos=player:get_pos()}
  274. end
  275. end)
  276. -- Called when a player dies
  277. -- `reason`: a PlayerHPChangeReason table, see register_on_player_hpchange
  278. -- Using the regular minetest.register_on_dieplayer causes the new callback to be inserted *after*
  279. -- the on_dieplayer used by the bones mod, which means the bones mod clears the player inventory before
  280. -- we get to this and we can't tell if there was a death compass in it.
  281. -- We must therefore rearrange the callback table to move this mod's callback to the front
  282. -- to ensure it always goes first.
  283. local death_compass_dieplayer_callback = table.remove(minetest.registered_on_dieplayers)
  284. table.insert(minetest.registered_on_dieplayers, 1, death_compass_dieplayer_callback)
  285. minetest.register_on_respawnplayer(function(player)
  286. local player_name = player:get_player_name()
  287. local compasses = player_death_location[player_name]
  288. if compasses then
  289. local inv = minetest.get_inventory({type="player", name=player_name})
  290. -- Remove any death compasses they might still have for some reason
  291. local current = inv:get_list("main")
  292. for i, item in pairs(current) do
  293. if item:get_name() == "death_compass:inactive" then
  294. current[i] = ItemStack("")
  295. end
  296. end
  297. inv:set_list("main", current)
  298. -- give them new compasses pointing to their place of death
  299. for i = 1, compasses.count do
  300. local compass = ItemStack("death_compass:dir0")
  301. set_target(compass, compasses.pos, player_name)
  302. inv:add_item("main", compass)
  303. end
  304. end
  305. return false
  306. end)
  307. -- * Called when player is to be respawned
  308. -- * Called _before_ repositioning of player occurs
  309. -- * return true in func to disable regular player placement
  310. minetest.register_on_leaveplayer(function(player, timed_out)
  311. hide_hud(player, player:get_player_name())
  312. end)