init.lua 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. --
  2. -- Drowning
  3. --
  4. -- Copyright (c) 2012 by randomproof, Casimir, cheesecake.
  5. --
  6. -- A Minetest mod that simulates suffocating/drowning whenever the players
  7. -- stay in liquid fluids longer than a certain period of time.
  8. --
  9. -- Basically, the mod works as follows:
  10. --
  11. -- * The player's breath is tracked by the holding_breath variable, which
  12. -- contains the number of seconds the player is currently holding his/her
  13. -- breath. The time begins at zero whenever the user begins to poke his/her
  14. -- head into a liquid. It is reset whenever the player dies or gets out of
  15. -- the liquid.
  16. --
  17. -- * If the player cannot hold his/her breath any longer, damage is being dealt
  18. -- in discrete steps, until either the player gets out of the liquid or dies.
  19. -- In other words, this simulates the player drowning. The discrete steps are
  20. -- remembered by the variable next_scheduled_damage, which effectively is the
  21. -- number of seconds the player is holding his/her breath. So, once
  22. -- holding_breath reaches the value of next_scheduled_damage, damage is dealt,
  23. -- and next_scheduled_damage is being set to the next point in time where damage
  24. -- will be dealt again.
  25. --
  26. -- The time where the player cannot hold his/her breath any longer is determined
  27. -- by the constant START_DROWNING_SECONDS.
  28. --
  29. -- * The discrete time offsets for dealing damage due to drowning are shortened
  30. -- every time. So, if T is the period that has been used for calculating the
  31. -- last value of next_scheduled_damage, the new period is some T1, where T1 < T.
  32. -- Actually, T1 is a function of T: T1(T) = T * FACTOR_DROWNING_SECONDS.
  33. --
  34. -- This shortening is however restricted by MIN_DROWNING_SECONDS, so time offsets
  35. -- won't be shortened any more if they would be less than that amount.
  36. -- (So, we actually have T1(T) = max(MIN_DROWNING_SECONDS, T * FACTOR_DROWNING_SECONDS).)
  37. --
  38. --
  39. drowning = {} -- Exported functions
  40. local holding_breath = {} -- How long have players been holding their breath?
  41. local scheduling_interval = {} -- Offset used for calculating the next schedule damage.
  42. local next_scheduled_damage = {} -- Next time when drowning is accounted for.
  43. local player_bubbles = {} -- Number of half bubbles shown in hud.
  44. local file = minetest.get_worldpath() .. "/drowning"
  45. local START_DROWNING_SECONDS = 40 -- Time that you can hold your breath unaffected.
  46. local FACTOR_DROWNING_SECONDS = 0.5 -- Each scheduled damage offset is shortened by this.
  47. local MIN_DROWNING_SECONDS = 1 -- Scheduled damage offsets won't be shorter than this.
  48. local DROWNING_DAMAGE = 1 -- The drowning damage dealt per scheduled offset.
  49. local MIN_TIME_SLICE = 0.5 -- Minimum number of seconds that must pass before
  50. -- the system actually does some expensive calculations.
  51. local timer = 0
  52. if minetest.settings:get_bool("enable_damage") == true then
  53. -- no_drown privilege
  54. minetest.register_privilege("no_drown", {
  55. description = "Player is not drowning",
  56. give_to_singleplayer = false
  57. })
  58. local function init_drown_state(name)
  59. if scheduling_interval[name] == nil then
  60. scheduling_interval[name] = START_DROWNING_SECONDS
  61. end
  62. if next_scheduled_damage[name] == nil then
  63. next_scheduled_damage[name] = START_DROWNING_SECONDS
  64. end
  65. end
  66. local function reset_drown_state(player)
  67. player:set_attribute("h_breath", nil)
  68. local name = player:get_player_name()
  69. scheduling_interval[name] = START_DROWNING_SECONDS
  70. next_scheduled_damage[name] = START_DROWNING_SECONDS
  71. -- Don't display breath in hud.
  72. local player = minetest.get_player_by_name(name)
  73. player:hud_remove(player_bubbles[name])
  74. player_bubbles[name] = nil
  75. end
  76. local function is_player_in_liquid(player)
  77. local pos = player:get_pos()
  78. pos.x = math.floor(pos.x+0.5)
  79. pos.y = math.floor(pos.y+2.0)
  80. pos.z = math.floor(pos.z+0.5)
  81. local n_head = minetest.get_node(pos).name
  82. if n_head == "ignore" then return end -- No change on startup.
  83. -- Check if node is liquid (0=not 2=lava 3=water).
  84. if minetest.get_item_group(n_head, "liquid") ~= 0 then
  85. return true
  86. end
  87. end
  88. local function play_drown_sound(player, filename, hear_distance)
  89. local headpos = player:get_pos()
  90. headpos.y = headpos.y + 1
  91. minetest.sound_play(filename,
  92. {pos = headpos, gain = 1.0, max_hear_distance = hear_distance})
  93. end
  94. local function schedule_next_damage(name)
  95. scheduling_interval[name] = math.floor(scheduling_interval[name]*FACTOR_DROWNING_SECONDS)
  96. if scheduling_interval[name] < MIN_DROWNING_SECONDS then
  97. scheduling_interval[name] = MIN_DROWNING_SECONDS
  98. end
  99. next_scheduled_damage[name] = next_scheduled_damage[name] + scheduling_interval[name]
  100. end
  101. local function on_drown(player)
  102. local name = player:get_player_name()
  103. -- get_attribut always returns a string, so we have to convert it.
  104. local h_breath = tonumber(player:get_attribute("h_breath") or 0)
  105. if h_breath >= next_scheduled_damage[name] then
  106. if player:get_hp() > 0 then
  107. -- Player is still alive, so:
  108. -- deal damage, play sound and schedule next damage
  109. local new_hp = math.max(0, (player:get_hp() - DROWNING_DAMAGE))
  110. player:set_hp(new_hp)
  111. minetest.chat_send_player(name, "You are drowning.")
  112. schedule_next_damage(name)
  113. else
  114. -- Player has died; reset drowning state.
  115. reset_drown_state(player)
  116. end
  117. end
  118. end
  119. local function on_gasp(player)
  120. reset_drown_state(player)
  121. play_drown_sound(player, "drowning_gasp", 32)
  122. end
  123. -- Display the remaining breath in hud.
  124. function drowning.update_bar(player)
  125. local name = player:get_player_name()
  126. local bubbles = 0
  127. if scheduling_interval[name] > 1 then
  128. local h_breath = tonumber(player:get_attribute("h_breath") or 0)
  129. bubbles = math.ceil(20*((next_scheduled_damage[name] - h_breath)/scheduling_interval[name]))
  130. end
  131. if player_bubbles[name] then
  132. player:hud_change(player_bubbles[name], "number", bubbles)
  133. else
  134. player_bubbles[name] = player:hud_add({
  135. hud_elem_type = "statbar",
  136. position = {x=0.5,y=1.0},
  137. text = "bubble.png",
  138. number = 20,
  139. dir = 1,
  140. offset = {x=(9*24)-6,y=-(4*24+8)},
  141. size = {x=16, y=16},
  142. })
  143. end
  144. end
  145. -- Main function
  146. minetest.register_globalstep(function(dtime)
  147. timer = timer + dtime
  148. if timer >= MIN_TIME_SLICE then
  149. timer = timer - MIN_TIME_SLICE
  150. else return end
  151. for _,player in ipairs(minetest.get_connected_players()) do
  152. local name = player:get_player_name()
  153. if minetest.get_player_privs(name)["no_drown"] then
  154. if player_bubbles[name] then
  155. reset_drown_state(player)
  156. end
  157. return
  158. end
  159. local h_breath = tonumber(player:get_attribute("h_breath") or 0)
  160. init_drown_state(name)
  161. if is_player_in_liquid(player) then
  162. h_breath = h_breath + MIN_TIME_SLICE
  163. player:set_attribute("h_breath", h_breath)
  164. drowning.update_bar(player)
  165. on_drown(player)
  166. elseif h_breath > 0 then
  167. on_gasp(player)
  168. end
  169. end
  170. end)
  171. minetest.register_on_respawnplayer(function(player)
  172. reset_drown_state(player)
  173. end)
  174. end