init.lua 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. stamina = {players = {}, mod = "redo"}
  2. STAMINA_TICK = tonumber(minetest.settings:get("stamina_tick")) or 800
  3. -- time in seconds after that 1 stamina point is taken
  4. STAMINA_TICK_MIN = 4 -- stamina ticks won't reduce stamina below this level
  5. STAMINA_HEALTH_TICK = 4 -- time in seconds after player gets healed/damaged
  6. STAMINA_MOVE_TICK = 0.5 -- time in seconds after the movement is checked
  7. STAMINA_POISON_TICK = 1.25 -- time in seconds after player is poisoned
  8. STAMINA_EXHAUST_DIG = 2 -- exhaustion increased this value after digged node
  9. STAMINA_EXHAUST_PLACE = 1 -- .. after digging node
  10. STAMINA_EXHAUST_MOVE = 1.5 -- .. if player movement detected
  11. STAMINA_EXHAUST_JUMP = 5 -- .. if jumping
  12. STAMINA_EXHAUST_CRAFT = 2 -- .. if player crafts
  13. STAMINA_EXHAUST_PUNCH = 40 -- .. if player punches another player
  14. STAMINA_EXHAUST_LVL = 160 -- at what exhaustion player saturation gets lowered
  15. STAMINA_HEAL = 1 -- number of HP player gets healed after STAMINA_HEALTH_TICK
  16. STAMINA_HEAL_LVL = 5 -- lower level of saturation needed to get healed
  17. STAMINA_STARVE = 1 -- number of HP player gets damaged by stamina after
  18. -- STAMINA_HEALTH_TICK
  19. STAMINA_STARVE_LVL = 3 -- level of staturation that causes starving
  20. STAMINA_VISUAL_MAX = 20 -- hud bar extends only to 20
  21. SPRINT_SPEED = 0.3 -- how much faster player can run if satiated
  22. SPRINT_JUMP = 0.1 -- how much higher player can jump if satiated
  23. SPRINT_DRAIN = 0.35 -- how fast to drain satation while sprinting (0-1)
  24. local function get_int_attribute(player)
  25. -- pipeworks fake player check
  26. if not player or not player.get_attribute then
  27. return nil
  28. end
  29. local meta = player:get_meta()
  30. local level = meta and meta:get_string("stamina:level")
  31. if level then
  32. return tonumber(level)
  33. end
  34. return nil
  35. end
  36. local stamina_enabled = minetest.settings:get_bool("enable_stamina") ~= false
  37. local function stamina_update_level(player, level)
  38. -- pipeworks fake player check
  39. if not player.get_attribute or not stamina_enabled then
  40. return nil
  41. end
  42. local old = get_int_attribute(player)
  43. if level == old then -- To suppress HUD update
  44. return
  45. end
  46. -- players without interact priv cannot eat
  47. if not minetest.check_player_privs(player, {interact = true}) then
  48. return
  49. end
  50. local meta = player and player:get_meta() ; if not meta then return end
  51. meta:set_string("stamina:level", level)
  52. player:hud_change(
  53. stamina.players[player:get_player_name()].hud_id,
  54. "number",
  55. math.min(STAMINA_VISUAL_MAX, level)
  56. )
  57. end
  58. -- global function for mods to amend stamina level
  59. stamina.change = function(player, change)
  60. local name = player:get_player_name()
  61. if not name or not change or change == 0 then
  62. return false
  63. end
  64. local level = get_int_attribute(player) + change
  65. if level < 0 then level = 0 end
  66. if level > STAMINA_VISUAL_MAX then level = STAMINA_VISUAL_MAX end
  67. stamina_update_level(player, level)
  68. return true
  69. end
  70. local function exhaust_player(player, v)
  71. if not player
  72. or not player.is_player
  73. or not player:is_player()
  74. or not player.set_attribute then
  75. return
  76. end
  77. local name = player:get_player_name()
  78. if not name then
  79. return
  80. end
  81. local exhaustion = stamina.players[name].exhaustion + v
  82. if exhaustion > STAMINA_EXHAUST_LVL then
  83. exhaustion = 0
  84. local h = get_int_attribute(player)
  85. if h > 0 then
  86. stamina_update_level(player, h - 1)
  87. end
  88. end
  89. stamina.players[name].exhaustion = exhaustion
  90. end
  91. -- Sprint settings and function
  92. local enable_sprint = minetest.settings:get_bool("sprint") ~= false
  93. local enable_sprint_particles = minetest.settings:get_bool("sprint_particles") ~= false
  94. local monoids = minetest.get_modpath("player_monoids")
  95. local pova_mod = minetest.get_modpath("pova")
  96. local function set_sprinting(name, sprinting)
  97. local player = minetest.get_player_by_name(name)
  98. -- get player physics
  99. local def = player:get_physics_override()
  100. --print ("---", def.speed, def.jump)
  101. if sprinting == true and not stamina.players[name].sprint then
  102. if monoids then
  103. stamina.players[name].sprint = player_monoids.speed:add_change(
  104. player, def.speed + SPRINT_SPEED)
  105. stamina.players[name].jump = player_monoids.jump:add_change(
  106. player, def.jump + SPRINT_JUMP)
  107. elseif pova_mod then
  108. pova.add_override(name, "sprint",
  109. {speed = SPRINT_SPEED, jump = SPRINT_JUMP})
  110. pova.do_override(player)
  111. stamina.players[name].sprint = true
  112. else
  113. player:set_physics_override({
  114. speed = def.speed + SPRINT_SPEED,
  115. jump = def.jump + SPRINT_JUMP,
  116. })
  117. stamina.players[name].sprint = true
  118. end
  119. elseif sprinting == false
  120. and stamina.players[name]
  121. and stamina.players[name].sprint then
  122. if monoids then
  123. player_monoids.speed:del_change(player, stamina.players[name].speed)
  124. player_monoids.jump:del_change(player, stamina.players[name].jump)
  125. stamina.players[name].sprint = nil
  126. stamina.players[name].jump = nil
  127. elseif pova_mod then
  128. pova.del_override(name, "sprint")
  129. pova.do_override(player)
  130. stamina.players[name].sprint = nil
  131. else
  132. player:set_physics_override({
  133. speed = def.speed - SPRINT_SPEED,
  134. jump = def.jump - SPRINT_JUMP,
  135. })
  136. stamina.players[name].sprint = nil
  137. end
  138. end
  139. end
  140. local function head_particle(player, texture)
  141. local prop = player:get_properties()
  142. local pos = player:get_pos() ; pos.y = pos.y + prop.eye_height -- mouth level
  143. local dir = player:get_look_dir()
  144. minetest.add_particlespawner({
  145. amount = 5,
  146. time = 0.1,
  147. minpos = pos,
  148. maxpos = pos,
  149. minvel = {x = dir.x - 1, y = dir.y, z = dir.z - 1},
  150. maxvel = {x = dir.x + 1, y = dir.y, z = dir.z + 1},
  151. minacc = {x = 0, y = -5, z = 0},
  152. maxacc = {x = 0, y = -9, z = 0},
  153. minexptime = 1,
  154. maxexptime = 1,
  155. minsize = 1,
  156. maxsize = 2,
  157. texture = texture
  158. })
  159. end
  160. local function drunk_tick()
  161. for _,player in ipairs(minetest.get_connected_players()) do
  162. local name = player:get_player_name()
  163. if name
  164. and stamina.players[name]
  165. and stamina.players[name].drunk then
  166. -- play burp sound every 20 seconds when drunk
  167. local num = stamina.players[name].drunk
  168. if num and num > 0 and math.floor(num / 20) == num / 20 then
  169. head_particle(player, "bubble.png")
  170. minetest.sound_play("stamina_burp",
  171. {to_player = name, gain = 0.7}, true)
  172. end
  173. stamina.players[name].drunk = stamina.players[name].drunk - 1
  174. if stamina.players[name].drunk < 1 then
  175. stamina.players[name].drunk = nil
  176. stamina.players[name].units = 0
  177. if not stamina.players[name].poisoned then
  178. player:hud_change(stamina.players[name].hud_id,
  179. "text", "stamina_hud_fg.png")
  180. end
  181. end
  182. -- effect only works when not riding boat/cart/horse etc.
  183. if not player:get_attach() then
  184. local yaw = player:get_look_horizontal() + math.random(-0.5, 0.5)
  185. player:set_look_horizontal(yaw)
  186. end
  187. end
  188. end
  189. end
  190. local function health_tick()
  191. for _,player in ipairs(minetest.get_connected_players()) do
  192. local air = player:get_breath() or 0
  193. local hp = player:get_hp()
  194. local h = get_int_attribute(player)
  195. local name = player:get_player_name()
  196. if name then
  197. -- damage player by 1 hp if saturation is < 2
  198. if h and h < STAMINA_STARVE_LVL
  199. and hp > 0 then
  200. player:set_hp(hp - STAMINA_STARVE, {hunger = true})
  201. end
  202. -- don't heal if drowning or dead or poisoned
  203. if h and h >= STAMINA_HEAL_LVL
  204. and h >= hp
  205. and hp > 0
  206. and air > 0
  207. and not stamina.players[name].poisoned then
  208. player:set_hp(hp + STAMINA_HEAL)
  209. stamina_update_level(player, h - 1)
  210. end
  211. end
  212. end
  213. end
  214. local function action_tick()
  215. for _,player in ipairs(minetest.get_connected_players()) do
  216. local controls = player and player:get_player_control()
  217. -- Determine if the player is walking or jumping
  218. if controls then
  219. if controls.jump then
  220. exhaust_player(player, STAMINA_EXHAUST_JUMP)
  221. elseif controls.up
  222. or controls.down
  223. or controls.left
  224. or controls.right then
  225. exhaust_player(player, STAMINA_EXHAUST_MOVE)
  226. end
  227. end
  228. --- START sprint
  229. if enable_sprint then
  230. local name = player and player:get_player_name()
  231. -- check if player can sprint (stamina must be over 6 points)
  232. if name
  233. and stamina.players[name]
  234. and not stamina.players[name].poisoned
  235. and not stamina.players[name].drunk
  236. and controls and controls.aux1 and controls.up
  237. and not minetest.check_player_privs(player, {fast = true})
  238. and get_int_attribute(player) > 6 then
  239. set_sprinting(name, true)
  240. -- create particles behind player when sprinting
  241. if enable_sprint_particles then
  242. local pos = player:get_pos()
  243. local node = minetest.get_node({
  244. x = pos.x,
  245. y = pos.y - 1,
  246. z = pos.z
  247. })
  248. if node.name ~= "air" then
  249. minetest.add_particlespawner({
  250. amount = 5,
  251. time = 0.01,
  252. minpos = {x = pos.x - 0.25, y = pos.y + 0.1, z = pos.z - 0.25},
  253. maxpos = {x = pos.x + 0.25, y = pos.y + 0.1, z = pos.z + 0.25},
  254. minvel = {x = -0.5, y = 1, z = -0.5},
  255. maxvel = {x = 0.5, y = 2, z = 0.5},
  256. minacc = {x = 0, y = -5, z = 0},
  257. maxacc = {x = 0, y = -12, z = 0},
  258. minexptime = 0.25,
  259. maxexptime = 0.5,
  260. minsize = 0.5,
  261. maxsize = 1.0,
  262. vertical = false,
  263. collisiondetection = false,
  264. texture = "default_dirt.png"
  265. })
  266. end
  267. end
  268. -- Lower the player's stamina when sprinting
  269. local level = get_int_attribute(player)
  270. stamina_update_level(player,
  271. level - (SPRINT_DRAIN * STAMINA_MOVE_TICK))
  272. elseif name then
  273. set_sprinting(name, false)
  274. end
  275. end
  276. -- END sprint
  277. end
  278. end
  279. local function poison_tick()
  280. for _,player in ipairs(minetest.get_connected_players()) do
  281. local name = player and player:get_player_name()
  282. if name
  283. and stamina.players[name]
  284. and stamina.players[name].poisoned
  285. and stamina.players[name].poisoned > 0 then
  286. stamina.players[name].poisoned =
  287. stamina.players[name].poisoned - 1
  288. local hp = player:get_hp() - 1
  289. head_particle(player, "stamina_poison_particle.png")
  290. if hp > 0 then
  291. player:set_hp(hp, {poison = true})
  292. end
  293. elseif name
  294. and stamina.players[name]
  295. and stamina.players[name].poisoned then
  296. if not stamina.players[name].drunk then
  297. player:hud_change(stamina.players[name].hud_id,
  298. "text", "stamina_hud_fg.png")
  299. end
  300. stamina.players[name].poisoned = nil
  301. end
  302. end
  303. end
  304. local function stamina_tick()
  305. for _,player in ipairs(minetest.get_connected_players()) do
  306. local h = get_int_attribute(player)
  307. if h and h > STAMINA_TICK_MIN then
  308. stamina_update_level(player, h - 1)
  309. end
  310. end
  311. end
  312. -- Time based stamina functions
  313. local stamina_timer, health_timer, action_timer, poison_timer, drunk_timer = 0,0,0,0,0
  314. local function stamina_globaltimer(dtime)
  315. stamina_timer = stamina_timer + dtime
  316. health_timer = health_timer + dtime
  317. action_timer = action_timer + dtime
  318. poison_timer = poison_timer + dtime
  319. drunk_timer = drunk_timer + dtime
  320. -- simulate drunk walking (thanks LumberJ)
  321. if drunk_timer > 1.0 then
  322. drunk_tick() ; drunk_timer = 0
  323. end
  324. -- hurt player when poisoned
  325. if poison_timer > STAMINA_POISON_TICK then
  326. poison_tick() ; poison_timer = 0
  327. end
  328. -- sprint control and particle animation
  329. if action_timer > STAMINA_MOVE_TICK then
  330. action_tick() ; action_timer = 0
  331. end
  332. -- lower saturation by 1 point after STAMINA_TICK
  333. if stamina_timer > STAMINA_TICK then
  334. stamina_tick() ; stamina_timer = 0
  335. end
  336. -- heal or damage player, depending on saturation
  337. if health_timer > STAMINA_HEALTH_TICK then
  338. health_tick() ; health_timer = 0
  339. end
  340. end
  341. -- override core.do_item_eat() so we can redirect hp_change to stamina
  342. core.do_item_eat = function(hp_change, replace_with_item, itemstack, user, pointed_thing)
  343. if user.is_fake_player then
  344. return -- abort if called by fake player (eg. pipeworks-wielder)
  345. end
  346. local old_itemstack = itemstack
  347. itemstack = stamina.eat(
  348. hp_change, replace_with_item, itemstack, user, pointed_thing)
  349. for _, callback in pairs(core.registered_on_item_eats) do
  350. local result = callback(hp_change, replace_with_item, itemstack, user,
  351. pointed_thing, old_itemstack)
  352. if result then
  353. return result
  354. end
  355. end
  356. return itemstack
  357. end
  358. -- not local since it's called from within core context
  359. function stamina.eat(hp_change, replace_with_item, itemstack, user, pointed_thing)
  360. if not itemstack or not user then
  361. return itemstack
  362. end
  363. local level = get_int_attribute(user) or 0
  364. if level >= STAMINA_VISUAL_MAX then
  365. return itemstack
  366. end
  367. local name = user:get_player_name()
  368. if hp_change > 0 then
  369. stamina_update_level(user, level + hp_change)
  370. elseif hp_change < 0 then
  371. -- assume hp_change < 0
  372. user:hud_change(stamina.players[name].hud_id, "text", "stamina_hud_poison.png")
  373. stamina.players[name].poisoned = -hp_change
  374. end
  375. -- if {drink=1} group set then use sip sound instead of default eat
  376. local snd = "stamina_eat"
  377. local itemname = itemstack:get_name()
  378. local def = minetest.registered_items[itemname]
  379. if def and def.groups and def.groups.drink then
  380. snd = "stamina_sip"
  381. end
  382. minetest.sound_play(snd, {to_player = name, gain = 0.7}, true)
  383. -- particle effect when eating
  384. local texture = minetest.registered_items[itemname].inventory_image
  385. head_particle(user, texture)
  386. -- if player drinks milk then stop poison and being drunk
  387. local item_name = itemstack:get_name() or ""
  388. if item_name == "mobs:bucket_milk"
  389. or item_name == "mobs:glass_milk"
  390. or item_name == "farming:soy_milk" then
  391. stamina.players[name].poisoned = 0
  392. stamina.players[name].drunk = 0
  393. end
  394. itemstack:take_item()
  395. if replace_with_item then
  396. if itemstack:is_empty() then
  397. itemstack:add_item(replace_with_item)
  398. else
  399. local inv = user:get_inventory()
  400. if inv:room_for_item("main", {name = replace_with_item}) then
  401. inv:add_item("main", replace_with_item)
  402. else
  403. local pos = user:get_pos()
  404. if pos then core.add_item(pos, replace_with_item) end
  405. end
  406. end
  407. end
  408. -- check for alcohol
  409. local units = minetest.registered_items[itemname].groups
  410. and minetest.registered_items[itemname].groups.alcohol or 0
  411. if units > 0 then
  412. stamina.players[name].units = (stamina.players[name].units or 0) + 1
  413. if stamina.players[name].units > 3 then
  414. stamina.players[name].drunk = 60
  415. stamina.players[name].units = 0
  416. user:hud_change(stamina.players[name].hud_id, "text",
  417. "stamina_hud_poison.png")
  418. minetest.chat_send_player(name,
  419. minetest.get_color_escape_sequence("#1eff00")
  420. .. "You suddenly feel tipsy!")
  421. end
  422. end
  423. return itemstack
  424. end
  425. -- stamina is disabled if damage is disabled
  426. if minetest.settings:get_bool("enable_damage")
  427. and minetest.settings:get_bool("enable_stamina") ~= false then
  428. minetest.register_on_joinplayer(function(player)
  429. local level = STAMINA_VISUAL_MAX -- TODO
  430. if get_int_attribute(player) then
  431. level = math.min(get_int_attribute(player), STAMINA_VISUAL_MAX)
  432. else
  433. local meta = player:get_meta()
  434. meta:set_string("stamina:level", level)
  435. end
  436. local name = player:get_player_name()
  437. local id = player:hud_add({
  438. name = "stamina",
  439. hud_elem_type = "statbar",
  440. position = {x = 0.5, y = 1},
  441. size = {x = 24, y = 24},
  442. text = "stamina_hud_fg.png",
  443. number = level,
  444. alignment = {x = -1, y = -1},
  445. offset = {x = -266, y = -110},
  446. max = 0
  447. })
  448. stamina.players[name] = {
  449. hud_id = id,
  450. exhaustion = 0,
  451. poisoned = nil,
  452. drunk = nil,
  453. sprint = nil
  454. }
  455. end)
  456. minetest.register_on_respawnplayer(function(player)
  457. local name = player:get_player_name()
  458. if stamina.players[name].poisoned
  459. or stamina.players[name].drunk then
  460. player:hud_change(stamina.players[name].hud_id, "text", "stamina_hud_fg.png")
  461. end
  462. stamina.players[name].exhaustion = 0
  463. stamina.players[name].poisoned = nil
  464. stamina.players[name].drunk = nil
  465. stamina.players[name].sprint = nil
  466. stamina_update_level(player, STAMINA_VISUAL_MAX)
  467. end)
  468. minetest.register_globalstep(stamina_globaltimer)
  469. minetest.register_on_placenode(function(pos, oldnode, player, ext)
  470. exhaust_player(player, STAMINA_EXHAUST_PLACE)
  471. end)
  472. minetest.register_on_dignode(function(pos, oldnode, player, ext)
  473. exhaust_player(player, STAMINA_EXHAUST_DIG)
  474. end)
  475. minetest.register_on_craft(function(itemstack, player, old_craft_grid, craft_inv)
  476. exhaust_player(player, STAMINA_EXHAUST_CRAFT)
  477. end)
  478. minetest.register_on_punchplayer(function(player, hitter, time_from_last_punch,
  479. tool_capabilities, dir, damage)
  480. exhaust_player(hitter, STAMINA_EXHAUST_PUNCH)
  481. end)
  482. else
  483. -- create player table on join
  484. minetest.register_on_joinplayer(function(player)
  485. stamina.players[player:get_player_name()] = {
  486. poisoned = nil, sprint = nil, drunk = nil, exhaustion = 0}
  487. end)
  488. end
  489. -- clear when player leaves
  490. minetest.register_on_leaveplayer(function(player)
  491. stamina.players[player:get_player_name()] = nil
  492. end)
  493. --lucky blocks (if damage and stamina active)
  494. if minetest.get_modpath("lucky_block")
  495. and minetest.settings:get_bool("enable_damage")
  496. and minetest.settings:get_bool("enable_stamina") ~= false then
  497. local effect_me = function(pos, player, def)
  498. local green = minetest.get_color_escape_sequence("#bada55")
  499. local name = player:get_player_name()
  500. if def.poison or def.drunk then
  501. player:hud_change(stamina.players[name].hud_id,
  502. "text", "stamina_hud_poison.png")
  503. end
  504. if def.poison and def.poison > 0 then
  505. stamina.players[name].poisoned = def.poison
  506. minetest.chat_send_player(name,
  507. green .. "Seems you have been poisoned!")
  508. elseif def.drunk and def.drunk > 0 then
  509. stamina.players[name].drunk = def.drunk
  510. minetest.chat_send_player(name,
  511. green .. "You seem a little tipsy!")
  512. end
  513. end
  514. lucky_block:add_blocks({
  515. {"cus", effect_me, {poison = 5} },
  516. {"cus", effect_me, {poison = 10} },
  517. {"cus", effect_me, {drunk = 30} },
  518. })
  519. end