Repixture/mods/rp_hunger/init.lua

605 lines
16 KiB
Lua

--
-- Hunger mod
-- originally from Nodetest
-- Tweaked by Kaadmy and Wuzzy, for Repixture
--
local S = minetest.get_translator("rp_hunger")
hunger = {}
-- If enabled, show advanced player hunger values
local HUNGER_DEBUG = minetest.settings:get_bool("hunger_debug", false)
-- Maximum possible hunger value
hunger.MAX_HUNGER = 20
-- Maximum possible saturation value
hunger.MAX_SATURATION = 100
-- Player heals if hunger is equal to or greater than this value
local HUNGER_HEAL_LEVEL = 16
-- Player starves (takes damage) if hunger is equal to or lower than this value
local HUNGER_STARVE_LEVEL = 0
-- Check if player needs healing every this number of hunger steps (see hunger_step setting below)
-- E.g. if hunger_step is 3.0 and this value is 5, player can be healed every 15 seconds (3*5).
local HEAL_EVERY_N_HEALTH_STEPS = 5
-- Warng the player about being hungry if hunger level drops to one of these values
local HUNGER_WARNING_1 = 5 -- first warning
local HUNGER_WARNING_2 = 3 -- second warning, must be lower than the first one
-- Player speed penalty when eating (speed multiplier)
local EATING_SPEED = 0.6
-- How long the speed penalty applies, in seconds
local EATING_SPEED_DURATION = 2.0
local mod_achievements = minetest.get_modpath("rp_achievements") ~= nil
-- Per-player userdata
local userdata = {}
local particlespawners = {}
local player_step = {}
local player_health_step = {}
local player_bar = {}
local player_debughud = {}
local hunger_file = minetest.get_worldpath() .. "/hunger.dat"
local saving = false
-- Seconds per hunger update, 2.0 is slightly fast
local timer_interval = tonumber(minetest.settings:get("hunger_step")) or 3.0
timer_interval = math.max(0.0, timer_interval)
local timer = 0
-- Loading and saving
local function save_hunger()
local f = io.open(hunger_file, "w")
for name, data in pairs(userdata) do
f:write(data.hunger .. " " .. data.saturation .. " " .. name .. "\n")
end
io.close(f)
saving = false
end
local function delayed_save()
if not saving then
saving = true
minetest.after(40, save_hunger)
end
end
local function load_hunger()
local f = io.open(hunger_file, "r")
if f then
repeat
local hnger = f:read("*n")
local sat = f:read("*n")
local name = f:read("*l")
if name == nil or sat == nil then
break
end
name = name:sub(2)
if not userdata[name] then
userdata[name] = {
hunger = hunger.MAX_HUNGER,
active = 0,
moving = 0,
saturation = 0,
}
end
if hnger then
userdata[name].hunger = hnger
end
if sat then
userdata[name].saturation = sat
end
until f:read(0) == nil
io.close(f)
else
save_hunger()
end
end
local function on_load()
load_hunger()
end
local function on_shutdown()
save_hunger()
end
local function update_bar(player)
if not player then
return
end
local name = player:get_player_name()
if HUNGER_DEBUG then
if player_debughud[name] then
local text = "Hunger Debug:\n"
if minetest.settings:get_bool("hunger_enable", true) then
text = text .. "hunger = " .. tostring(userdata[name].hunger) .. "\n"
text = text .. "saturation = " .. tostring(userdata[name].saturation) .. "\n"
text = text .. "moving = " .. tostring(userdata[name].moving) .. "\n"
text = text .. "active = " .. tostring(userdata[name].active) .. "\n"
text = text .. "step = " .. tostring(player_step[name]) .. "\n"
else
text = text .. "<hunger disabled>\n"
end
text = text .. "health_step = " .. tostring(player_health_step[name])
player:hud_change(player_debughud[name], "text", text)
else
player_debughud[name] = player:hud_add(
{
hud_elem_type = "text",
position = {x=0.75,y=1.0},
text = "",
number = 0xFFFFFFFF,
alignment = {x=-1, y=-1},
scale = {x=100, y=100},
size = {x=1, y=1},
offset = {x=-32, y=-32},
z_index = 1,
})
end
if minetest.settings:get_bool("hunger_enable", true) == false then
return
end
end
if player_bar[name] then
player:hud_change(player_bar[name], "number", userdata[name].hunger)
else
player_bar[name] = player:hud_add(
{
hud_elem_type = "statbar",
position = {x=0.5,y=1.0},
text = "hunger.png",
text2 = "hunger.png^[colorize:#666666:255",
number = userdata[name].hunger,
item = hunger.MAX_HUNGER,
dir = 0,
size = {x=24, y=24},
offset = {x=16, y=-(48+24+24)},
z_index = 1,
})
end
end
local function on_dignode(pos, oldnode, player)
if not player then
return
end
local name = player:get_player_name()
if not userdata[name] then
return
end
userdata[name].active = userdata[name].active + 2
if HUNGER_DEBUG then
update_bar(player)
end
end
local function on_placenode(pos, node, player)
if not player then
return
end
local name = player:get_player_name()
userdata[name].active = userdata[name].active + 2
if HUNGER_DEBUG then
update_bar(player)
end
end
local function on_joinplayer(player)
local name = player:get_player_name()
if not userdata[name] then
userdata[name] = {
hunger = hunger.MAX_HUNGER,
active = 0,
moving = 0,
saturation = 0,
}
end
update_bar(player)
end
local function on_leaveplayer(player)
local name = player:get_player_name()
player_bar[name] = nil
player_debughud[name] = nil
userdata[name] = nil
end
local function on_respawnplayer(player)
local name = player:get_player_name()
userdata[name].hunger = hunger.MAX_HUNGER
userdata[name].saturation = 0
userdata[name].active = 0
userdata[name].moving = 0
player_step[name] = 0
player_health_step[name] = 0
update_bar(player)
delayed_save()
end
local function on_respawnplayer_nohunger(player)
local name = player:get_player_name()
player_health_step[name] = 0
if HUNGER_DEBUG then
update_bar(player)
end
end
local function on_item_eat(hpdata, replace_with_item, itemstack,
player, pointed_thing)
if not player then return end
if not hpdata then return end
local hp_change = 0
local saturation = 2
if type(hpdata) == "number" then
hp_change = hpdata
else
hp_change = hpdata.hp
saturation = hpdata.sat
end
local name = player:get_player_name()
userdata[name].hunger = userdata[name].hunger + hp_change
userdata[name].hunger = math.min(hunger.MAX_HUNGER, userdata[name].hunger)
userdata[name].saturation = math.min(hunger.MAX_SATURATION, userdata[name].saturation
+ saturation)
local headpos = player:get_pos()
headpos.y = headpos.y + 1
minetest.sound_play("hunger_eat", {pos = headpos, max_hear_distance = 8, object=player}, true)
particlespawners[name] = minetest.add_particlespawner(
{
amount = 10,
time = 0.1,
minpos = {x = headpos.x - 0.3, y = headpos.y - 0.3, z = headpos.z - 0.3},
maxpos = {x = headpos.x + 0.3, y = headpos.y + 0.3, z = headpos.z + 0.3},
minvel = {x = -1, y = -1, z = -1},
maxvel = {x = 1, y = 0, z = 1},
minacc = {x = 0, y = 6, z = 0},
maxacc = {x = 0, y = 1, z = 0},
minexptime = 0.5,
maxexptime = 1,
minsize = 0.5,
maxsize = 2,
texture = {
name = "magicpuff.png",
scale_tween = { 1, 0, start = 0.75 },
},
})
minetest.after(0.15, function(name)
if particlespawners[name] then
minetest.delete_particlespawner(particlespawners[name])
end
end, name)
if mod_achievements then
achievements.trigger_subcondition(player, "eat_everything", itemstack:get_name())
end
player_effects.apply_effect(player, "hunger_eating")
update_bar(player)
delayed_save()
if not minetest.is_creative_enabled(name) then
itemstack:take_item(1)
end
return itemstack
end
-- Healing routine for on_globalstep below
-- Heals player if this function was called enough times
-- (HEAL_EVERY_N_HEALTH_STEPS to be precise)
-- and player has a high enough hunger value (HUNGER_HEAL_LEVEL).
-- * player: Player to heal
-- * phunger: current player hunger. Can be nil, then hunger will be ignored
local function health_step(player, phunger)
local name = player:get_player_name()
if player_health_step[name] == nil then
player_health_step[name] = 0
end
player_health_step[name] = player_health_step[name] + 1
local hp = player:get_hp()
if player_health_step[name] >= HEAL_EVERY_N_HEALTH_STEPS then
player_health_step[name] = HEAL_EVERY_N_HEALTH_STEPS
if hp > 0 and hp < minetest.PLAYER_MAX_HP_DEFAULT and (phunger == nil or phunger >= HUNGER_HEAL_LEVEL) then
player_health_step[name] = 0
player:set_hp(hp+1)
end
end
end
local function on_globalstep(dtime)
timer = timer + dtime
if timer < timer_interval then
return
end
timer = 0
for _,player in ipairs(minetest.get_connected_players()) do
local name = player:get_player_name()
local controls = player:get_player_control()
local moving = 0
if controls.up or controls.down or controls.left or controls.right then
moving = moving + 1
end
if controls.sneak and not controls.aux1 then
moving = moving - 1
end
if controls.jump then
moving = moving + 1
end
if controls.aux1 then -- sprinting
moving = moving + 3
end
userdata[name].moving = math.max(0, moving)
end
for _,player in ipairs(minetest.get_connected_players()) do
local name = player:get_player_name()
local hp = player:get_hp()
if userdata[name] == nil then
userdata[name] = {
hunger = hunger.MAX_HUNGER,
active = 0,
moving = 0,
saturation = 0,
}
end
if not player_step[name] then
player_step[name] = 0
end
userdata[name].active = userdata[name].active +
userdata[name].moving
player_step[name] = player_step[name] + userdata[name].active + 1
userdata[name].saturation = userdata[name].saturation - 1
if userdata[name].saturation <= 0 then
userdata[name].saturation = 0
if player_step[name] >= 24 then -- how much the player has been active
player_step[name] = 0
local oldhng = userdata[name].hunger
userdata[name].hunger = userdata[name].hunger - 1
if (oldhng == HUNGER_WARNING_1 or oldhng == HUNGER_WARNING_2) and hp >= 0 then
minetest.chat_send_player(name, minetest.colorize("#ff0", S("You are hungry.")))
local pos_sound = player:get_pos()
minetest.sound_play({name="hunger_hungry"}, {pos=pos_sound, max_hear_distance=3, object=player}, true)
end
if userdata[name].hunger <= HUNGER_STARVE_LEVEL and hp >= 0 then
local old_hp = hp
player:set_hp(hp - 1)
userdata[name].hunger = 0
if hp > 1 then
minetest.chat_send_player(name, minetest.colorize("#f00", S("You are starving.")))
elseif old_hp > 0 then
minetest.chat_send_player(name, minetest.colorize("#f00", S("You starved to death.")))
end
end
end
end
userdata[name].active = 0
health_step(player, userdata[name].hunger)
update_bar(player)
end
delayed_save()
end
-- Eating food when hunger is disabled.
-- This just removes the food.
local function fake_on_item_eat(hpdata, replace_with_item, itemstack,
player, pointed_thing)
local headpos = player:get_pos()
headpos.y = headpos.y + 1
minetest.sound_play(
"hunger_eat",
{
pos = headpos,
max_hear_distance = 8,
object = player,
}, true)
if mod_achievements then
achievements.trigger_subcondition(player, "eat_everything", itemstack:get_name())
end
if not minetest.is_creative_enabled(player:get_player_name()) then
itemstack:take_item(1)
end
return itemstack
end
-- If hunger is disabled, just heal players over time
local function on_globalstep_nohunger(dtime)
timer = timer + dtime
if timer < timer_interval then
return
end
timer = 0
for _,player in ipairs(minetest.get_connected_players()) do
health_step(player, nil)
if HUNGER_DEBUG then
update_bar(player)
end
end
end
if minetest.settings:get_bool("enable_damage") and minetest.settings:get_bool("hunger_enable", true) then
minetest.after(0, on_load)
minetest.register_on_shutdown(on_shutdown)
minetest.register_on_dignode(on_dignode)
minetest.register_on_placenode(on_placenode)
minetest.register_on_joinplayer(on_joinplayer)
minetest.register_on_leaveplayer(on_leaveplayer)
minetest.register_on_respawnplayer(on_respawnplayer)
minetest.register_on_item_eat(on_item_eat)
minetest.register_globalstep(on_globalstep)
-- Public API functions.
-- Note this mod itself sets the hunger and saturation directly
function hunger.get_hunger(playername)
return userdata[playername].hunger
end
function hunger.get_saturation(playername)
return userdata[playername].saturation
end
function hunger.set_hunger(playername, hnger)
userdata[playername].hunger = math.floor(math.max(0, math.min(hunger.MAX_HUNGER, hnger)))
local player = minetest.get_player_by_name(playername)
update_bar(player)
end
function hunger.set_saturation(playername, saturation)
userdata[playername].saturation = math.floor(math.max(0, math.min(hunger.MAX_SATURATION, saturation)))
local player = minetest.get_player_by_name(playername)
update_bar(player)
end
else
minetest.register_on_leaveplayer(on_leaveplayer)
minetest.register_on_item_eat(fake_on_item_eat)
minetest.register_on_respawnplayer(on_respawnplayer_nohunger)
minetest.register_globalstep(on_globalstep_nohunger)
-- Public API functions are no-op if hunger disabled
function hunger.get_hunger() return nil end
function hunger.get_saturation() return nil end
function hunger.set_hunger() return end
function hunger.set_saturation() return end
end
player_effects.register_effect(
"hunger_eating",
{
title = S("Eating"),
description = S("You're eating food, which slows you down"),
duration = EATING_SPEED_DURATION,
physics = {
speed = EATING_SPEED,
},
icon = "rp_hunger_effect_eating.png",
})
if mod_achievements then
minetest.register_on_mods_loaded(function()
local all_foods, all_foods_readable = {}, {}
for k, v in pairs(minetest.registered_items) do
if minetest.get_item_group(k, "food") > 0 then
table.insert(all_foods, k)
table.insert(all_foods_readable, ItemStack(v):get_short_description())
end
end
achievements.register_achievement(
"eat_everything",
{
title = S("Gourmet"),
description = S("Eat everything that can be eaten."),
subconditions = all_foods,
subconditions_readable = all_foods_readable,
times = 0,
icon = "rp_hunger_achievement_eat_everything.png",
})
end)
end
minetest.register_chatcommand("hunger", {
description = S("Set hunger level of player or yourself"),
privs = { server = true },
params = S("[<player>] <hunger>"),
func = function(playername, param)
-- Set hunger of specified target player
local target, hungr = string.match(param, "^([a-zA-Z0-9-_]+) ([0-9]+)$")
if target and hungr then
hungr = tonumber(hungr)
if not hungr then
return false
end
local player = minetest.get_player_by_name(target)
if not player then
return false, S("Player is not online.")
end
hunger.set_hunger(target, hungr)
return true
end
-- Set hunger of commander
local hungr = string.match(param, "^([0-9]+)$")
hungr = tonumber(hungr)
if not hungr then
return false
end
local player = minetest.get_player_by_name(playername)
if not player then
return false, S("No player.")
end
hunger.set_hunger(playername, hungr)
return true
end
})