diff --git a/mods/stamina/README.txt b/mods/stamina/README.txt new file mode 100644 index 00000000..5a453089 --- /dev/null +++ b/mods/stamina/README.txt @@ -0,0 +1,72 @@ +Minetest mod "Stamina" +===================== + +(C) 2015 - BlockMen +(C) 2016 - Auke Kok + + +About this mod: +--------------- +This mod adds a stamina, or "hunger" mechanic to Minetest. Actions like +crafting, walking, digging or fighting make the player exhausted. When +enough exhaustion has been accumulated, the player gets more hungry, +and loses stamina. + +If a player is low on stamina, they start taking periodical damage, +and ultimately will die if they do not eat food. + +Eating food no longer heals the player. Instead, it increases the +stamina of the player. The stamina bar shows how well fed the player +is. More bread pieces means more stamina. + +Walking while holding down Aux1 (usually E key) will make player sprint so +long as their stamina bar is 3 or more bread. This will make the player run +that bit faster and jump a tiny bit higher. + +Q&A time: Why won't I move the stamina bar to the right? + +Answer: this conflicts with the builtin breath bar. To move the +builtin breath bar, I basically have to entirely re-implement it +in lua including timers to catch breath changes for each online +player, which is all a waste of time, just to move a few pixels +around. + + +For Modders: +------------ +This mod intercepts minetest.item_eat(), and applies the hp_change +as stamina change. The value can be positive (increase stamina) or +negative (periodically damage the player by 1 hp). + +Callbacks that are registered via minetest.register_on_item_eat() +are called after this mod, so the itemstack will have changed already +when callbacks are called. You can get the original itemstack as 6th +parameter of your function then. + +A global function is available for mods to change player stamina levels: + + stamina.change(player, change) + + +TenPlus1 Additions: +------------------- + + - Added support for POVA and player_monoids + - Added Pipeworks checks for fake players + - Added 60 second drunk effect when foods have {alcohol=1} group (eat 4 or more) + - Moved exhaustion and hud_id to player table instead of player attributes + + +License: +-------- +Code: +- all code LGPL-2.1+ +Textures: +- stamina_hud_poison.png - BlockMen (CC-BY 3.0) +- stamina_hud_fg.png - PilzAdam (WTFPL), modified by BlockMen +- stamina_hud_bg.png - PilzAdam (WTFPL), modified by BlockMen +Sounds: +- stamina_eat.*.ogg - http://www.freesound.org/people/sonictechtonic/sounds/242215/ CC-BY-3.0 + +stamina_burp.ogg - https://www.freesfx.co.uk/sfx/burp (small burp) +stamina_sip.ogg - https://elements.envato.com/sip-16081-C7V5YLG diff --git a/mods/stamina/depends.txt b/mods/stamina/depends.txt new file mode 100644 index 00000000..7c7fa76b --- /dev/null +++ b/mods/stamina/depends.txt @@ -0,0 +1,4 @@ +default +3d_armor? +player_monoids? +pova? diff --git a/mods/stamina/description.txt b/mods/stamina/description.txt new file mode 100644 index 00000000..46395a10 --- /dev/null +++ b/mods/stamina/description.txt @@ -0,0 +1 @@ +Adds stamina, hunger and drunk effects. diff --git a/mods/stamina/init.lua b/mods/stamina/init.lua new file mode 100644 index 00000000..8acc1575 --- /dev/null +++ b/mods/stamina/init.lua @@ -0,0 +1,684 @@ + +stamina = {players = {}} + +STAMINA_TICK = tonumber(minetest.settings:get("stamina_tick")) or 800 + -- time in seconds after that 1 stamina point is taken +STAMINA_TICK_MIN = 4 -- stamina ticks won't reduce stamina below this level +STAMINA_HEALTH_TICK = 4 -- time in seconds after player gets healed/damaged +STAMINA_MOVE_TICK = 0.5 -- time in seconds after the movement is checked +STAMINA_POISON_TICK = 1.25 -- time in seconds after player is poisoned + +STAMINA_EXHAUST_DIG = 3 -- exhaustion increased this value after digged node +STAMINA_EXHAUST_PLACE = 1 -- .. after digging node +STAMINA_EXHAUST_MOVE = 1.5 -- .. if player movement detected +STAMINA_EXHAUST_JUMP = 5 -- .. if jumping +STAMINA_EXHAUST_CRAFT = 20 -- .. if player crafts +STAMINA_EXHAUST_PUNCH = 40 -- .. if player punches another player +STAMINA_EXHAUST_LVL = 160 -- at what exhaustion player saturation gets lowered + +STAMINA_HEAL = 1 -- number of HP player gets healed after STAMINA_HEALTH_TICK +STAMINA_HEAL_LVL = 5 -- lower level of saturation needed to get healed +STAMINA_STARVE = 1 -- number of HP player gets damaged by stamina after STAMINA_HEALTH_TICK +STAMINA_STARVE_LVL = 3 -- level of staturation that causes starving + +STAMINA_VISUAL_MAX = 20 -- hud bar extends only to 20 + +SPRINT_SPEED = 0.3 -- how much faster player can run if satiated +SPRINT_JUMP = 0.1 -- how much higher player can jump if satiated +SPRINT_DRAIN = 0.35 -- how fast to drain satation while sprinting (0-1) + + +local function get_int_attribute(player) + + -- pipeworks fake player check + if not player or not player.get_attribute then + return nil + end + + local meta = player:get_meta() + local level = meta and meta:get_string("stamina:level") + + if level then + return tonumber(level) + end + + return nil +end + + +local stamina_enabled = minetest.settings:get_bool("enable_stamina") ~= false + +local function stamina_update_level(player, level) + + -- pipeworks fake player check + if not player.get_attribute or not stamina_enabled then + return nil + end + + local old = get_int_attribute(player) + + if level == old then -- To suppress HUD update + return + end + + -- players without interact priv cannot eat + if not minetest.check_player_privs(player, {interact = true}) then + return + end + + local meta = player and player:get_meta() ; if not meta then return end + + meta:set_string("stamina:level", level) + + player:hud_change( + stamina.players[player:get_player_name()].hud_id, + "number", + math.min(STAMINA_VISUAL_MAX, level) + ) +end + + +-- global function for mods to amend stamina level +stamina.change = function(player, change) + + local name = player:get_player_name() + + if not name or not change or change == 0 then + return false + end + + local level = get_int_attribute(player) + change + + if level < 0 then level = 0 end + + if level > STAMINA_VISUAL_MAX then level = STAMINA_VISUAL_MAX end + + stamina_update_level(player, level) + + return true +end + + +local function exhaust_player(player, v) + + if not player + or not player.is_player + or not player:is_player() + or not player.set_attribute then + return + end + + local name = player:get_player_name() + + if not name then + return + end + + local exhaustion = stamina.players[name].exhaustion + v + + if exhaustion > STAMINA_EXHAUST_LVL then + + exhaustion = 0 + + local h = get_int_attribute(player) + + if h > 0 then + stamina_update_level(player, h - 1) + end + end + + stamina.players[name].exhaustion = exhaustion +end + + +-- Sprint settings and function +local enable_sprint = minetest.settings:get_bool("sprint") ~= false +local enable_sprint_particles = minetest.settings:get_bool("sprint_particles") ~= false +local monoids = minetest.get_modpath("player_monoids") +local pova_mod = minetest.get_modpath("pova") + + +local function set_sprinting(name, sprinting) + + local player = minetest.get_player_by_name(name) + + -- get player physics + local def = player:get_physics_override() + +--print ("---", def.speed, def.jump) + + if sprinting == true and not stamina.players[name].sprint then + + if monoids then + + stamina.players[name].sprint = player_monoids.speed:add_change( + player, def.speed + SPRINT_SPEED) + + stamina.players[name].jump = player_monoids.jump:add_change( + player, def.jump + SPRINT_JUMP) + + elseif pova_mod then + + pova.add_override(name, "sprint", + {speed = SPRINT_SPEED, jump = SPRINT_JUMP}) + + pova.do_override(player) + + stamina.players[name].sprint = true + else + player:set_physics_override({ + speed = def.speed + SPRINT_SPEED, + jump = def.jump + SPRINT_JUMP, + }) + + stamina.players[name].sprint = true + end + + elseif sprinting == false + and stamina.players[name] + and stamina.players[name].sprint then + + if monoids then + + player_monoids.speed:del_change(player, stamina.players[name].speed) + player_monoids.jump:del_change(player, stamina.players[name].jump) + + stamina.players[name].sprint = nil + stamina.players[name].jump = nil + + elseif pova_mod then + + pova.del_override(name, "sprint") + pova.do_override(player) + + stamina.players[name].sprint = nil + else + player:set_physics_override({ + speed = def.speed - SPRINT_SPEED, + jump = def.jump - SPRINT_JUMP, + }) + + stamina.players[name].sprint = nil + end + end +end + + +local function drunk_tick() + + for _,player in ipairs(minetest.get_connected_players()) do + + local name = player:get_player_name() + + if name + and stamina.players[name] + and stamina.players[name].drunk then + + -- play burp sound every 20 seconds when drunk + local num = stamina.players[name].drunk + + if num and num > 0 and math.floor(num / 20) == num / 20 then + minetest.sound_play("stamina_burp", + {to_player = name, gain = 0.7}, true) + end + + stamina.players[name].drunk = stamina.players[name].drunk - 1 + + if stamina.players[name].drunk < 1 then + + stamina.players[name].drunk = nil + stamina.players[name].units = 0 + + if not stamina.players[name].poisoned then + + player:hud_change(stamina.players[name].hud_id, + "text", "stamina_hud_fg.png") + end + end + + -- effect only works when not riding boat/cart/horse etc. + if not player:get_attach() then + + local yaw = player:get_look_horizontal() + math.random(-0.5, 0.5) + + player:set_look_horizontal(yaw) + end + end + end +end + + +local function health_tick() + + for _,player in ipairs(minetest.get_connected_players()) do + + local air = player:get_breath() or 0 + local hp = player:get_hp() + local h = get_int_attribute(player) + local name = player:get_player_name() + + if name then + + -- damage player by 1 hp if saturation is < 2 + if h and h < STAMINA_STARVE_LVL + and hp > 0 then + player:set_hp(hp - STAMINA_STARVE, {hunger = true}) + end + + -- don't heal if drowning or dead or poisoned + if h and h >= STAMINA_HEAL_LVL + and h >= hp + and hp > 0 + and air > 0 + and not stamina.players[name].poisoned then + + player:set_hp(hp + STAMINA_HEAL) + + stamina_update_level(player, h - 1) + end + end + end +end + + +local function action_tick() + + for _,player in ipairs(minetest.get_connected_players()) do + + local controls = player and player:get_player_control() + + -- Determine if the player is walking or jumping + if controls then + + if controls.jump then + exhaust_player(player, STAMINA_EXHAUST_JUMP) + + elseif controls.up + or controls.down + or controls.left + or controls.right then + exhaust_player(player, STAMINA_EXHAUST_MOVE) + end + end + + --- START sprint + if enable_sprint then + + local name = player and player:get_player_name() + + -- check if player can sprint (stamina must be over 6 points) + if name + and stamina.players[name] + and not stamina.players[name].poisoned + and not stamina.players[name].drunk + and controls and controls.aux1 and controls.up + and not minetest.check_player_privs(player, {fast = true}) + and get_int_attribute(player) > 6 then + + set_sprinting(name, true) + + -- create particles behind player when sprinting + if enable_sprint_particles then + + local pos = player:get_pos() + local node = minetest.get_node({ + x = pos.x, + y = pos.y - 1, + z = pos.z + }) + + if node.name ~= "air" then + + minetest.add_particlespawner({ + amount = 5, + time = 0.01, + minpos = {x = pos.x - 0.25, y = pos.y + 0.1, z = pos.z - 0.25}, + maxpos = {x = pos.x + 0.25, y = pos.y + 0.1, z = pos.z + 0.25}, + minvel = {x = -0.5, y = 1, z = -0.5}, + maxvel = {x = 0.5, y = 2, z = 0.5}, + minacc = {x = 0, y = -5, z = 0}, + maxacc = {x = 0, y = -12, z = 0}, + minexptime = 0.25, + maxexptime = 0.5, + minsize = 0.5, + maxsize = 1.0, + vertical = false, + collisiondetection = false, + texture = "default_dirt.png" + }) + + end + end + + -- Lower the player's stamina when sprinting + local level = get_int_attribute(player) + + stamina_update_level(player, + level - (SPRINT_DRAIN * STAMINA_MOVE_TICK)) + + elseif name then + set_sprinting(name, false) + end + end + -- END sprint + end +end + + +local function poison_tick() + + for _,player in ipairs(minetest.get_connected_players()) do + + local name = player and player:get_player_name() + + if name + and stamina.players[name] + and stamina.players[name].poisoned + and stamina.players[name].poisoned > 0 then + + stamina.players[name].poisoned = + stamina.players[name].poisoned - 1 + + local hp = player:get_hp() - 1 + + if hp > 0 then + player:set_hp(hp, {poison = true}) + end + + elseif name + and stamina.players[name] + and stamina.players[name].poisoned then + + if not stamina.players[name].drunk then + + player:hud_change(stamina.players[name].hud_id, + "text", "stamina_hud_fg.png") + end + + stamina.players[name].poisoned = nil + end + end +end + + +local function stamina_tick() + + for _,player in ipairs(minetest.get_connected_players()) do + + local h = get_int_attribute(player) + + if h and h > STAMINA_TICK_MIN then + stamina_update_level(player, h - 1) + end + end +end + + +-- Time based stamina functions +local stamina_timer, health_timer, action_timer, poison_timer, drunk_timer = 0,0,0,0,0 + +local function stamina_globaltimer(dtime) + + stamina_timer = stamina_timer + dtime + health_timer = health_timer + dtime + action_timer = action_timer + dtime + poison_timer = poison_timer + dtime + drunk_timer = drunk_timer + dtime + + -- simulate drunk walking (thanks LumberJ) + if drunk_timer > 1.0 then + drunk_tick() ; drunk_timer = 0 + end + + -- hurt player when poisoned + if poison_timer > STAMINA_POISON_TICK then + poison_tick() ; poison_timer = 0 + end + + -- sprint control and particle animation + if action_timer > STAMINA_MOVE_TICK then + action_tick() ; action_timer = 0 + end + + -- lower saturation by 1 point after STAMINA_TICK + if stamina_timer > STAMINA_TICK then + stamina_tick() ; stamina_timer = 0 + end + + -- heal or damage player, depending on saturation + if health_timer > STAMINA_HEALTH_TICK then + health_tick() ; health_timer = 0 + end +end + + +-- override core.do_item_eat() so we can redirect hp_change to stamina +core.do_item_eat = function(hp_change, replace_with_item, itemstack, user, pointed_thing) + + if user.is_fake_player then + return -- abort if called by fake player (eg. pipeworks-wielder) + end + + local old_itemstack = itemstack + + itemstack = stamina.eat( + hp_change, replace_with_item, itemstack, user, pointed_thing) + + for _, callback in pairs(core.registered_on_item_eats) do + + local result = callback(hp_change, replace_with_item, itemstack, user, + pointed_thing, old_itemstack) + + if result then + return result + end + end + + return itemstack +end + + +-- not local since it's called from within core context +function stamina.eat(hp_change, replace_with_item, itemstack, user, pointed_thing) + + if not itemstack or not user then + return itemstack + end + + local level = get_int_attribute(user) or 0 + + if level >= STAMINA_VISUAL_MAX then + return itemstack + end + + local name = user:get_player_name() + + if hp_change > 0 then + + stamina_update_level(user, level + hp_change) + + elseif hp_change < 0 then + + -- assume hp_change < 0 + user:hud_change(stamina.players[name].hud_id, "text", "stamina_hud_poison.png") + + stamina.players[name].poisoned = -hp_change + end + + -- if {drink=1} group set then use sip sound instead of default eat + local snd = "stamina_eat" + local itemname = itemstack:get_name() + local def = minetest.registered_items[itemname] + + if def and def.groups and def.groups.drink then + snd = "stamina_sip" + end + + minetest.sound_play(snd, {to_player = name, gain = 0.7}, true) + + -- particle effect when eating + local pos = user:get_pos() ; pos.y = pos.y + 1.5 -- mouth level + local texture = minetest.registered_items[itemname].inventory_image + local dir = user:get_look_dir() + + minetest.add_particlespawner({ + amount = 5, + time = 0.1, + minpos = pos, + maxpos = pos, + minvel = {x = dir.x - 1, y = dir.y, z = dir.z - 1}, + maxvel = {x = dir.x + 1, y = dir.y, z = dir.z + 1}, + minacc = {x = 0, y = -5, z = 0}, + maxacc = {x = 0, y = -9, z = 0}, + minexptime = 1, + maxexptime = 1, + minsize = 1, + maxsize = 2, + texture = texture + }) + + -- if player drinks milk then stop poison and being drunk + local item_name = itemstack:get_name() or "" + if item_name == "mobs:bucket_milk" + or item_name == "mobs:glass_milk" + or item_name == "farming:soy_milk" then + + stamina.players[name].poisoned = 0 + stamina.players[name].drunk = 0 + end + + itemstack:take_item() + + if replace_with_item then + + if itemstack:is_empty() then + itemstack:add_item(replace_with_item) + else + local inv = user:get_inventory() + + if inv:room_for_item("main", {name = replace_with_item}) then + inv:add_item("main", replace_with_item) + else + pos.y = math.floor(pos.y - 1.0) + core.add_item(pos, replace_with_item) + end + end + end + + -- check for alcohol + local units = minetest.registered_items[itemname].groups + and minetest.registered_items[itemname].groups.alcohol or 0 + + if units > 0 then + + stamina.players[name].units = (stamina.players[name].units or 0) + 1 + + if stamina.players[name].units > 3 then + + stamina.players[name].drunk = 60 + stamina.players[name].units = 0 + + user:hud_change(stamina.players[name].hud_id, "text", + "stamina_hud_poison.png") + + minetest.chat_send_player(name, + minetest.get_color_escape_sequence("#1eff00") + .. "You suddenly feel tipsy!") + end + end + + return itemstack +end + + +-- stamina is disabled if damage is disabled +if minetest.settings:get_bool("enable_damage") +and minetest.settings:get_bool("enable_stamina") ~= false then + + minetest.register_on_joinplayer(function(player) + + local level = STAMINA_VISUAL_MAX -- TODO + + if get_int_attribute(player) then + + level = math.min(get_int_attribute(player), + STAMINA_VISUAL_MAX) + else + local meta = player:get_meta() + + meta:set_string("stamina:level", level) + end + + local name = player:get_player_name() + + local id = player:hud_add({ + name = "stamina", + hud_elem_type = "statbar", + position = {x = 0.5, y = 1}, + size = {x = 24, y = 24}, + text = "stamina_hud_fg.png", + number = level, + alignment = {x = -1, y = -1}, + offset = {x = -266, y = -110}, + max = 0, + }) + + stamina.players[name] = { + hud_id = id, + exhaustion = 0, + poisoned = nil, + drunk = nil, + sprint = nil, + } + end) + + minetest.register_on_respawnplayer(function(player) + + local name = player:get_player_name() + + if stamina.players[name].poisoned + or stamina.players[name].drunk then + player:hud_change(stamina.players[name].hud_id, "text", "stamina_hud_fg.png") + end + + stamina.players[name].exhaustion = 0 + stamina.players[name].poisoned = nil + stamina.players[name].drunk = nil + stamina.players[name].sprint = nil + + stamina_update_level(player, STAMINA_VISUAL_MAX) + end) + + minetest.register_globalstep(stamina_globaltimer) + + minetest.register_on_placenode(function(pos, oldnode, player, ext) + exhaust_player(player, STAMINA_EXHAUST_PLACE) + end) + + minetest.register_on_dignode(function(pos, oldnode, player, ext) + exhaust_player(player, STAMINA_EXHAUST_DIG) + end) + + minetest.register_on_craft(function(itemstack, player, old_craft_grid, craft_inv) + exhaust_player(player, STAMINA_EXHAUST_CRAFT) + end) + + minetest.register_on_punchplayer(function(player, hitter, time_from_last_punch, + tool_capabilities, dir, damage) + exhaust_player(hitter, STAMINA_EXHAUST_PUNCH) + end) + +else + + -- create player table on join + minetest.register_on_joinplayer(function(player) + stamina.players[player:get_player_name()] = { + poisoned = nil, sprint = nil, drunk = nil, exhaustion = 0} + end) + +end + +-- clear when player leaves +minetest.register_on_leaveplayer(function(player) + stamina.players[player:get_player_name()] = nil +end) diff --git a/mods/stamina/mod.conf b/mods/stamina/mod.conf new file mode 100644 index 00000000..fb091e75 --- /dev/null +++ b/mods/stamina/mod.conf @@ -0,0 +1,4 @@ +name = stamina +depends = default +optional_depends = 3d_armor, player_monoids, pova +description = Adds stamina, hunger and drunk effects.