diff --git a/mods/stamina/README.txt b/mods/stamina/README.txt new file mode 100644 index 00000000..b959c4f8 --- /dev/null +++ b/mods/stamina/README.txt @@ -0,0 +1,70 @@ +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 + diff --git a/mods/stamina/description.txt b/mods/stamina/description.txt new file mode 100644 index 00000000..1074b3d4 --- /dev/null +++ b/mods/stamina/description.txt @@ -0,0 +1 @@ +Adds stamina and hunger effects. diff --git a/mods/stamina/init.lua b/mods/stamina/init.lua new file mode 100644 index 00000000..8e8f8c3d --- /dev/null +++ b/mods/stamina/init.lua @@ -0,0 +1,621 @@ + +stamina = {players = {}} + +STAMINA_TICK = 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, key) + + -- pipeworks fake player check + if not player.get_attribute then + return nil + end + + local level = player:get_attribute(key) + + if level then + return tonumber(level) + end + + return nil +end + + +local function stamina_update_level(player, level) + + -- pipeworks fake player check + if not player.get_attribute then + return nil + end + + local old = get_int_attribute(player, "stamina:level") + + 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 + + player:set_attribute("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, "stamina:level") + 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, "stamina:level") + + 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.setting_getbool("sprint") ~= false +local enable_sprint_particles = minetest.setting_getbool("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].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 + + +-- Time based stamina functions +local stamina_timer = 0 +local health_timer = 0 +local action_timer = 0 +local poison_timer = 0 +local drunk_timer = 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 + + local players = minetest.get_connected_players() + + -- simulate drunk walking (thanks LumberJ) + if drunk_timer > 1.0 then + + for _,player in pairs(players) do + + local name = player:get_player_name() + + if stamina.players[name] + and stamina.players[name].drunk then + + 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 + + player:hud_change(stamina.players[name].hud_id, + "text", "stamina_hud_fg.png") + end + + -- effect only works when not riding boat/cart/horse etc. + if not player:get_attach() then + + local yaw = player:get_look_horizontal() + + yaw = yaw + math.random(-0.5, 0.5) + + player:set_look_horizontal(yaw) + end + end + + drunk_timer = 0 + end + end + + + -- hurt player when poisoned + if poison_timer > STAMINA_POISON_TICK then + + for _,player in pairs(players) do + + local name = player:get_player_name() + + if 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) + end + + elseif stamina.players[name].poisoned then + + player:hud_change(stamina.players[name].hud_id, + "text", "stamina_hud_fg.png") + + stamina.players[name].poisoned = nil + end + + poison_timer = 0 + end + end + + + -- sprint control and particle animation + if action_timer > STAMINA_MOVE_TICK then + + for _,player in pairs(players) do + + local controls = player:get_player_control() + + -- Determine if the player is walking + 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 + + --- START sprint + if enable_sprint then + + local name = player:get_player_name() + + -- check if player can sprint (stamina must be over 6 points) + if not stamina.players[name].poisoned + and not stamina.players[name].drunk + and controls.aux1 and controls.up + and not minetest.check_player_privs(player, {fast = true}) + and get_int_attribute(player, "stamina:level") > 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:level") + + stamina_update_level(player, + level - (SPRINT_DRAIN * STAMINA_MOVE_TICK)) + else + set_sprinting(name, false) + end + end + -- END sprint + + action_timer = 0 + end + end + + + + -- lower saturation by 1 point after STAMINA_TICK + if stamina_timer > STAMINA_TICK then + + for _,player in pairs(players) do + + local h = get_int_attribute(player, "stamina:level") + + if h > STAMINA_TICK_MIN then + stamina_update_level(player, h - 1) + end + + stamina_timer = 0 + end + end + + + -- heal or damage player, depending on saturation + if health_timer > STAMINA_HEALTH_TICK then + + for _,player in pairs(players) do + + local air = player:get_breath() or 0 + local hp = player:get_hp() + local h = get_int_attribute(player, "stamina:level") + local name = player:get_player_name() + + -- damage player by 1 hp if saturation is < 2 (of 30) + if h < STAMINA_STARVE_LVL + and hp > 0 then + player:set_hp(hp - STAMINA_STARVE) + end + + -- don't heal if drowning or dead or poisoned + if 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 + + health_timer = 0 + end + 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, "stamina:level") 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 + + minetest.sound_play("stamina_eat", {to_player = name, gain = 0.7}) + + -- particle effect when eating + local pos = user:get_pos() ; pos.y = pos.y + 1.5 -- mouth level + local itemname = itemstack:get_name() + 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, + }) + + 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.setting_getbool("enable_damage") +and minetest.setting_get("enable_stamina") ~= false then + + minetest.register_on_joinplayer(function(player) + + local level = STAMINA_VISUAL_MAX -- TODO + + if get_int_attribute(player, "stamina:level") then + + level = math.min(get_int_attribute(player, "stamina:level"), + STAMINA_VISUAL_MAX) + else + player:set_attribute("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..b049a5a0 --- /dev/null +++ b/mods/stamina/mod.conf @@ -0,0 +1 @@ +name = stamina diff --git a/mods/stamina/screenshot.png b/mods/stamina/screenshot.png new file mode 100644 index 00000000..f3776ef5 Binary files /dev/null and b/mods/stamina/screenshot.png differ