diff --git a/README.md b/README.md index 8cb10d5..6982a3e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This modpack includes: - minecart: Techage transportation system for oil and other items - towercrane: Simplifies the building of large techage plants - basic_materials: Needed items for many recipes +- stamina: The "hunger" mod from "minetest-mods" - doc: Ingame documentation mod, used for minecart and signs_bot - unified_inventory: Player's inventory with crafting guide, bags, and more. - tubelib2: Necessary library @@ -40,6 +41,15 @@ Required: Minetest Game ### History +#### 2020-06-23 + +Updates (see local readme files): +- mod stamina added +- autobahn, towercrane and stamina now use a commen player physics lockout mechanism +- signs_bot v1.03 with bugfixes +- techage manual expanded + + #### 2020-06-21 Updates (see local readme files): diff --git a/autobahn/init.lua b/autobahn/init.lua index 95c3962..e7efdba 100644 --- a/autobahn/init.lua +++ b/autobahn/init.lua @@ -36,19 +36,23 @@ end local function set_player_privs(player) local physics = player:get_physics_override() local meta = player:get_meta() - if meta and physics then - -- store the player privs default values - meta:set_int("autobahn_speed", physics.speed) - -- set operator privs - meta:set_int("autobahn_isactive", 1) - physics.speed = 3.5 - minetest.sound_play("autobahn_motor", { - pos = player:get_pos(), - gain = 0.5, - max_hear_distance = 5, - }) - -- write back - player:set_physics_override(physics) + -- Check access conflicts with other mods + if meta:get_int("player_physics_locked") == 0 then + meta:set_int("player_physics_locked", 1) + if meta and physics then + -- store the player privs default values + meta:set_int("autobahn_speed", physics.speed) + -- set operator privs + meta:set_int("autobahn_isactive", 1) + physics.speed = 3.5 + minetest.sound_play("autobahn_motor", { + pos = player:get_pos(), + gain = 0.5, + max_hear_distance = 5, + }) + -- write back + player:set_physics_override(physics) + end end end @@ -64,6 +68,7 @@ local function reset_player_privs(player) meta:set_string("autobahn_speed", "") -- write back player:set_physics_override(physics) + meta:set_int("player_physics_locked", 0) end end @@ -79,6 +84,11 @@ minetest.register_on_leaveplayer(function(player) end end) +minetest.register_on_respawnplayer(function(player) + if is_active(player) then + reset_player_privs(player) + end +end) local function control_player(player) local player_name = player:get_player_name() @@ -152,7 +162,7 @@ local function update_node(pos) local nnode local npos -- check case 1 - facedir = (2 + node.param2) % 4 + local facedir = (2 + node.param2) % 4 npos = vector.add(pos, Facedir2Dir[facedir]) npos.y = npos.y - 1 nnode = minetest.get_node(npos) diff --git a/player_physics_design_pattern.md b/player_physics_design_pattern.md new file mode 100644 index 0000000..a448893 --- /dev/null +++ b/player_physics_design_pattern.md @@ -0,0 +1,71 @@ +# Player Physics Design Pattern + +To be able to control the access to player physics (like speed, gravity) and privs (like fast, fly) +a common design pattern is used for the following mod-pack mods: + +- autobahn (fast, speed) +- towercrane (fly, speed) +- ta4_jetpack (gravity, speed) +- stamina (resets the gravity/speed cyclically) +- 3d_armor (changes physics based on APi calls) + +All of these mods try to change the player physics, which is a common resource and should only be changed by one mod. + +This lockout design pattern takes care that only one mod at a time is able to change physics or privs. + +```lua +local function change_player_physics(player) + local physics = player:get_physics_override() + local meta = player:get_meta() + + -- Check access conflicts with other mods + if meta:get_int("player_physics_locked") == 0 then + meta:set_int("player_physics_locked", 1) + + -- store old values, here speed and gravity + meta:set_int("mymod_normal_player_speed", physics.speed) + meta:set_int("mymod_normal_player_gravity", physics.gravity) + + -- do whatever is needed + + player:set_physics_override(physics) + meta:set_int("mymod_is_active", 1) + end +end + +local function restore_player_physics(player) + local physics = player:get_physics_override() + local meta = player:get_meta() + + if meta:get_int("mymod_is_active") == 1 then + meta:set_int("mymod_is_active", 0) + + -- restore old values (speed and gravity) + physics.speed = meta:get_int("mymod_normal_player_speed") + physics.gravity = meta:get_int("mymod_normal_player_gravity") + player:set_physics_override(physics) + end + meta:set_int("player_physics_locked", 0) +end + + + +minetest.register_on_joinplayer(function(player) + restore_player_physics(player) +end) + +minetest.register_on_respawnplayer(function(player) + restore_player_physics(player) +end) + +-- optional +minetest.register_on_leaveplayer(function(player) + restore_player_physics(player) +end) + +-- optional +minetest.register_on_dieplayer(function(player) + restore_player_physics(player) +end) +``` + diff --git a/stamina/.luacheckrc b/stamina/.luacheckrc new file mode 100644 index 0000000..a6b4003 --- /dev/null +++ b/stamina/.luacheckrc @@ -0,0 +1,20 @@ +unused_args = false +allow_defined_top = true + +read_globals = { + "DIR_DELIM", + "minetest", + "dump", + "vector", "nodeupdate", + "VoxelManip", "VoxelArea", + "PseudoRandom", "ItemStack", + "intllib", + "default", + "armor", + "player_monoids", +} + +globals = { + minetest = { fields = { "do_item_eat" }}, +} + diff --git a/stamina/API.txt b/stamina/API.txt new file mode 100644 index 0000000..334a2d0 --- /dev/null +++ b/stamina/API.txt @@ -0,0 +1,64 @@ +stamina.get_saturation(player) + Gets a player's saturation + +stamina.set_saturation(player, level) + Set a player's saturation. + Updates the HUD to reflect saturation level. + +stamina.update_saturation(player, level) + Update's a player's saturation. + Callbacks are processed first. + +stamina.change_saturation(player, change) + Update a player's saturation by a delta, instead of an absolute value. + +stamina.register_on_update_saturation(function(player, level)) + Register a callback for when saturation is updated. + If the callback returns True, no further callbacks are called, + and the default behavior is bypassed. +----------------------- +stamina.is_poisoned(player) + Check if a player is poisoned + +stamina.set_poisoned(player, poisoned) + Set a player's "poisoned" status (boolean) + Updates the HUD to reflect poison status. + +stamina.poison(player, ticks, interval) + Poison a player for a certain amount of time. + Ticks is how many times to poison the player. + Interval is the time between poison ticks. + Processes callbacks before changing status. + +stamina.register_on_poison(function(player, ticks, interval)) + Register a callback for when a player is poisoned. + If the callback returns True, no further callbacks are called, + and the default behavior is bypassed. +------------------------- +stamina.get_exhaustion(player) + Gets a player's exhaustion level. + Value is between 0 and `stamina.exhaust_lvl` (default: 160) + +stamina.set_exhaustion(player, level) + Sets a player's exhaustion level. + +stamina.exhaust_player(player, change, cause) + Update a player's exhaustion by "change". + If the player's exhaustion exceeds `stamina.exhaust_lvl`, stamina + is taken and exhaustion is reset. + Cause is an optional argument indicating the origin of the exhaustion. + Callbacks are processed before exhausting the player. + +stamina.register_on_exhaust_player(function(player, change, cause)) + Register a callback for when exhaustion is updated. + If the callback returns True, no further callbacks are called, + and the default behavior is bypassed. +------------------------ +stamina.set_sprinting(name, sprinting) + Sets whether a player is sprinting or not. + Callbacks are processed before setting sprint status. + +stamina.register_on_sprinting(function(player, sprinting)) + Register a callback for when a player's sprinting status is updated. + If the callback returns True, no further callbacks are called, + and the default behavior is bypassed. diff --git a/stamina/README.txt b/stamina/README.txt new file mode 100644 index 0000000..388c32b --- /dev/null +++ b/stamina/README.txt @@ -0,0 +1,50 @@ +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. + + +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). + +See API.txt for information on the API. + +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/stamina/bower.json b/stamina/bower.json new file mode 100644 index 0000000..3436201 --- /dev/null +++ b/stamina/bower.json @@ -0,0 +1,14 @@ +{ + "name": "stamina", + "description": "This mod adds a stamina, or hunger mechanic to Minetest. Actions like crafting, walking, digging or fighting make the player exhausted.", + "homepage": "https://github.com/minetest-mods/stamina", + "authors": [ + "BlockMen - 2015", + "Auke Kok - 2016" + ], + "license": "LGPL-2.1+", + "keywords": [ + "hunger", + "stamina" + ] +} \ No newline at end of file diff --git a/stamina/depends.txt b/stamina/depends.txt new file mode 100644 index 0000000..bdb6c20 --- /dev/null +++ b/stamina/depends.txt @@ -0,0 +1,3 @@ +default +3d_armor? +player_monoids? diff --git a/stamina/description.txt b/stamina/description.txt new file mode 100644 index 0000000..1074b3d --- /dev/null +++ b/stamina/description.txt @@ -0,0 +1 @@ +Adds stamina and hunger effects. diff --git a/stamina/init.lua b/stamina/init.lua new file mode 100644 index 0000000..77d6d36 --- /dev/null +++ b/stamina/init.lua @@ -0,0 +1,552 @@ +if not minetest.settings:get_bool("enable_damage") then + minetest.log("warning", "[stamina] Stamina will not load if damage is disabled (enable_damage=false)") + return +end + +stamina = {} +local modname = minetest.get_current_modname() +local armor_mod = minetest.get_modpath("3d_armor") and minetest.global_exists("armor") and armor.def +local player_monoids_mod = minetest.get_modpath("player_monoids") and minetest.global_exists("player_monoids") + +function stamina.log(level, message, ...) + return minetest.log(level, ("[%s] %s"):format(modname, message:format(...))) +end + +local function get_setting(key, default) + local value = minetest.settings:get("stamina." .. key) + local num_value = tonumber(value) + if value and not num_value then + stamina.log("warning", "Invalid value for setting %s: %q. Using default %q.", key, value, default) + end + return num_value or default +end + +stamina.settings = { + -- see settingtypes.txt for descriptions + eat_particles = minetest.settings:get_bool("stamina.eat_particles", true), + sprint = minetest.settings:get_bool("stamina.sprint", true), + sprint_particles = minetest.settings:get_bool("stamina.sprint_particles", true), + sprint_lvl = get_setting("sprint_lvl", 6), + sprint_speed = get_setting("sprint_speed", 0.8), + sprint_jump = get_setting("sprint_jump", 0.1), + sprint_with_fast = minetest.settings:get_bool("stamina.sprint_with_fast", false), + tick = get_setting("tick", 800), + tick_min = get_setting("tick_min", 4), + health_tick = get_setting("health_tick", 4), + move_tick = get_setting("move_tick", 0.5), + poison_tick = get_setting("poison_tick", 2.0), + exhaust_dig = get_setting("exhaust_dig", 3), + exhaust_place = get_setting("exhaust_place", 1), + exhaust_move = get_setting("exhaust_move", 1.5), + exhaust_jump = get_setting("exhaust_jump", 5), + exhaust_craft = get_setting("exhaust_craft", 20), + exhaust_punch = get_setting("exhaust_punch", 40), + exhaust_sprint = get_setting("exhaust_sprint", 28), + exhaust_lvl = get_setting("exhaust_lvl", 160), + heal = get_setting("heal", 1), + heal_lvl = get_setting("heal_lvl", 5), + starve = get_setting("starve", 1), + starve_lvl = get_setting("starve_lvl", 3), + visual_max = get_setting("visual_max", 20), +} +local settings = stamina.settings + +local attribute = { + saturation = "stamina:level", + poisoned = "stamina:poisoned", + exhaustion = "stamina:exhaustion", +} + +local function is_player(player) + return ( + minetest.is_player(player) and + not player.is_fake_player + ) +end + +local function set_player_attribute(player, key, value) + if player.get_meta then + if value == nil then + player:get_meta():set_string(key, "") + else + player:get_meta():set_string(key, tostring(value)) + end + else + player:set_attribute(key, value) + end +end + +local function get_player_attribute(player, key) + if player.get_meta then + return player:get_meta():get_string(key) + else + return player:get_attribute(key) + end +end + +local hud_ids_by_player_name = {} + +local function get_hud_id(player) + return hud_ids_by_player_name[player:get_player_name()] +end + +local function set_hud_id(player, hud_id) + hud_ids_by_player_name[player:get_player_name()] = hud_id +end + +--- SATURATION API --- +function stamina.get_saturation(player) + return tonumber(get_player_attribute(player, attribute.saturation)) +end + +function stamina.set_saturation(player, level) + set_player_attribute(player, attribute.saturation, level) + player:hud_change( + get_hud_id(player), + "number", + math.min(settings.visual_max, level) + ) +end + +stamina.registered_on_update_saturations = {} +function stamina.register_on_update_saturation(fun) + table.insert(stamina.registered_on_update_saturations, fun) +end + +function stamina.update_saturation(player, level) + for _, callback in ipairs(stamina.registered_on_update_saturations) do + local result = callback(player, level) + if result then + return result + end + end + + local old = stamina.get_saturation(player) + + if level == old then -- To suppress HUD update + return + end + + -- players without interact priv cannot eat + if old < settings.heal_lvl and not minetest.check_player_privs(player, {interact=true}) then + return + end + + stamina.set_saturation(player, level) +end + +function stamina.change_saturation(player, change) + if not is_player(player) or not change or change == 0 then + return false + end + local level = stamina.get_saturation(player) + change + level = math.max(level, 0) + level = math.min(level, settings.visual_max) + stamina.update_saturation(player, level) + return true +end + +stamina.change = stamina.change_saturation -- for backwards compatablity +--- END SATURATION API --- +--- POISON API --- +function stamina.is_poisoned(player) + return get_player_attribute(player, attribute.poisoned) == "yes" +end + +function stamina.set_poisoned(player, poisoned) + local hud_id = get_hud_id(player) + if poisoned then + player:hud_change(hud_id, "text", "stamina_hud_poison.png") + set_player_attribute(player, attribute.poisoned, "yes") + else + player:hud_change(hud_id, "text", "stamina_hud_fg.png") + set_player_attribute(player, attribute.poisoned, "no") + end +end + +local function poison_tick(player, ticks, interval, elapsed) + if not stamina.is_poisoned(player) then + return + elseif elapsed > ticks then + stamina.set_poisoned(player, false) + else + local hp = player:get_hp() - 1 + if hp > 0 then + player:set_hp(hp) + end + minetest.after(interval, poison_tick, player, ticks, interval, elapsed + 1) + end +end + +stamina.registered_on_poisons = {} +function stamina.register_on_poison(fun) + table.insert(stamina.registered_on_poisons, fun) +end + +function stamina.poison(player, ticks, interval) + for _, fun in ipairs(stamina.registered_on_poisons) do + local rv = fun(player, ticks, interval) + if rv == true then + return + end + end + if not is_player(player) then + return + end + stamina.set_poisoned(player, true) + poison_tick(player, ticks, interval, 0) +end +--- END POISON API --- +--- EXHAUSTION API --- +stamina.exhaustion_reasons = { + craft = "craft", + dig = "dig", + heal = "heal", + jump = "jump", + move = "move", + place = "place", + punch = "punch", + sprint = "sprint", +} + +function stamina.get_exhaustion(player) + return tonumber(get_player_attribute(player, attribute.exhaustion)) +end + +function stamina.set_exhaustion(player, exhaustion) + set_player_attribute(player, attribute.exhaustion, exhaustion) +end + +stamina.registered_on_exhaust_players = {} +function stamina.register_on_exhaust_player(fun) + table.insert(stamina.registered_on_exhaust_players, fun) +end + +function stamina.exhaust_player(player, change, cause) + for _, callback in ipairs(stamina.registered_on_exhaust_players) do + local result = callback(player, change, cause) + if result then + return result + end + end + + if not is_player(player) then + return + end + + local exhaustion = stamina.get_exhaustion(player) or 0 + + exhaustion = exhaustion + change + + if exhaustion >= settings.exhaust_lvl then + exhaustion = exhaustion - settings.exhaust_lvl + stamina.change(player, -1) + end + + stamina.set_exhaustion(player, exhaustion) +end +--- END EXHAUSTION API --- +--- SPRINTING API --- +stamina.registered_on_sprintings = {} +function stamina.register_on_sprinting(fun) + table.insert(stamina.registered_on_sprintings, fun) +end + +function stamina.set_sprinting(player, sprinting) + for _, fun in ipairs(stamina.registered_on_sprintings) do + local rv = fun(player, sprinting) + if rv == true then + return + end + end + + if player_monoids_mod then + if sprinting then + player_monoids.speed:add_change(player, 1 + settings.sprint_speed, "stamina:physics") + player_monoids.jump:add_change(player, 1 + settings.sprint_jump, "stamina:physics") + else + player_monoids.speed:del_change(player, "stamina:physics") + player_monoids.jump:del_change(player, "stamina:physics") + end + else + local def + if armor_mod then + -- Get player physics from 3d_armor mod + local name = player:get_player_name() + def = { + speed=armor.def[name].speed, + jump=armor.def[name].jump, + gravity=armor.def[name].gravity + } + else + def = { + speed=1, + jump=1, + gravity=1 + } + end + + if sprinting then + def.speed = def.speed + settings.sprint_speed + def.jump = def.jump + settings.sprint_jump + end + + -- Check access conflicts with other mods + if player:get_meta():get_int("player_physics_locked") == 0 then + player:set_physics_override(def) + end + end + + if settings.sprint_particles and sprinting then + local pos = player:getpos() + local node = minetest.get_node({x = pos.x, y = pos.y - 1, z = pos.z}) + local def = minetest.registered_nodes[node.name] or {} + local drawtype = def.drawtype + if drawtype ~= "airlike" and drawtype ~= "liquid" and drawtype ~= "flowingliquid" 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 +end +--- END SPRINTING API --- + +-- Time based stamina functions +local function move_tick() + for _,player in ipairs(minetest.get_connected_players()) do + local controls = player:get_player_control() + local is_moving = controls.up or controls.down or controls.left or controls.right + local velocity = player:get_player_velocity() + velocity.y = 0 + local horizontal_speed = vector.length(velocity) + local has_velocity = horizontal_speed > 0.05 + + if controls.jump then + stamina.exhaust_player(player, settings.exhaust_jump, stamina.exhaustion_reasons.jump) + elseif is_moving and has_velocity then + stamina.exhaust_player(player, settings.exhaust_move, stamina.exhaustion_reasons.move) + end + + if settings.sprint then + local can_sprint = ( + controls.aux1 and + not player:get_attach() and + (settings.sprint_with_fast or not minetest.check_player_privs(player, {fast = true})) and + stamina.get_saturation(player) > settings.sprint_lvl + ) + + if can_sprint then + stamina.set_sprinting(player, true) + if is_moving and has_velocity then + stamina.exhaust_player(player, settings.exhaust_sprint, stamina.exhaustion_reasons.sprint) + end + else + stamina.set_sprinting(player, false) + end + end + end +end + +local function stamina_tick() + -- lower saturation by 1 point after settings.tick second(s) + for _,player in ipairs(minetest.get_connected_players()) do + local saturation = stamina.get_saturation(player) + if saturation > settings.tick_min then + stamina.update_saturation(player, saturation - 1) + end + end +end + +local function health_tick() + -- heal or damage player, depending on saturation + for _,player in ipairs(minetest.get_connected_players()) do + local air = player:get_breath() or 0 + local hp = player:get_hp() + local saturation = stamina.get_saturation(player) + + -- don't heal if dead, drowning, or poisoned + local should_heal = ( + saturation >= settings.heal_lvl and + saturation >= hp and + hp > 0 and + air > 0 + and not stamina.is_poisoned(player) + ) + -- or damage player by 1 hp if saturation is < 2 (of 30) + local is_starving = ( + saturation < settings.starve_lvl and + hp > 0 + ) + + if should_heal then + player:set_hp(hp + settings.heal) + stamina.exhaust_player(player, settings.exhaust_lvl, stamina.exhaustion_reasons.heal) + elseif is_starving then + player:set_hp(hp - settings.starve) + end + end +end + +local stamina_timer = 0 +local health_timer = 0 +local action_timer = 0 + +local function stamina_globaltimer(dtime) + stamina_timer = stamina_timer + dtime + health_timer = health_timer + dtime + action_timer = action_timer + dtime + + if action_timer > settings.move_tick then + action_timer = 0 + move_tick() + end + + if stamina_timer > settings.tick then + stamina_timer = 0 + stamina_tick() + end + + if health_timer > settings.health_tick then + health_timer = 0 + health_tick() + end +end + +-- override minetest.do_item_eat() so we can redirect hp_change to stamina +stamina.core_item_eat = minetest.do_item_eat +function minetest.do_item_eat(hp_change, replace_with_item, itemstack, player, pointed_thing) + for _, callback in ipairs(minetest.registered_on_item_eats) do + local result = callback(hp_change, replace_with_item, itemstack, player, pointed_thing) + if result then + return result + end + end + + if not is_player(player) or not itemstack then + return itemstack + end + + local level = stamina.get_saturation(player) or 0 + if level >= settings.visual_max then + -- don't eat if player is full + return itemstack + end + + local itemname = itemstack:get_name() + if replace_with_item then + stamina.log("action", "%s eats %s for %s stamina, replace with %s", + player:get_player_name(), itemname, hp_change, replace_with_item) + else + stamina.log("action", "%s eats %s for %s stamina", + player:get_player_name(), itemname, hp_change) + end + minetest.sound_play("stamina_eat", {to_player = player:get_player_name(), gain = 0.7}) + + if hp_change > 0 then + stamina.change_saturation(player, hp_change) + stamina.set_exhaustion(player, 0) + else + -- assume hp_change < 0. + stamina.poison(player, -hp_change, settings.poison_tick) + end + + if settings.eat_particles then + -- particle effect when eating + local pos = player:getpos() + pos.y = pos.y + 1.5 -- mouth level + local texture = minetest.registered_items[itemname].inventory_image + local dir = player: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, + }) + end + + itemstack:take_item() + + if replace_with_item then + if itemstack:is_empty() then + itemstack:add_item(replace_with_item) + else + local inv = player:get_inventory() + if inv:room_for_item("main", {name=replace_with_item}) then + inv:add_item("main", replace_with_item) + else + local pos = player:getpos() + pos.y = math.floor(pos.y - 1.0) + minetest.add_item(pos, replace_with_item) + end + end + end + + return itemstack +end + +minetest.register_on_joinplayer(function(player) + local level = stamina.get_saturation(player) or settings.visual_max + 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.set_saturation(player, level) + set_hud_id(player, id) + -- reset poisoned + stamina.set_poisoned(player, false) + -- remove legacy hud_id from player metadata + set_player_attribute(player, "stamina:hud_id", nil) +end) + +minetest.register_on_leaveplayer(function(player) + set_hud_id(player, nil) +end) + +minetest.register_globalstep(stamina_globaltimer) + +minetest.register_on_placenode(function(pos, oldnode, player, ext) + stamina.exhaust_player(player, settings.exhaust_place, stamina.exhaustion_reasons.place) +end) +minetest.register_on_dignode(function(pos, oldnode, player, ext) + stamina.exhaust_player(player, settings.exhaust_dig, stamina.exhaustion_reasons.dig) +end) +minetest.register_on_craft(function(itemstack, player, old_craft_grid, craft_inv) + stamina.exhaust_player(player, settings.exhaust_craft, stamina.exhaustion_reasons.craft) +end) +minetest.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool_capabilities, dir, damage) + stamina.exhaust_player(hitter, settings.exhaust_punch, stamina.exhaustion_reasons.punch) +end) +minetest.register_on_respawnplayer(function(player) + stamina.update_saturation(player, settings.visual_max) +end) diff --git a/stamina/mod.conf b/stamina/mod.conf new file mode 100644 index 0000000..b049a5a --- /dev/null +++ b/stamina/mod.conf @@ -0,0 +1 @@ +name = stamina diff --git a/stamina/screenshot.png b/stamina/screenshot.png new file mode 100644 index 0000000..4ea1cb0 Binary files /dev/null and b/stamina/screenshot.png differ diff --git a/stamina/settingtypes.txt b/stamina/settingtypes.txt new file mode 100644 index 0000000..40592c0 --- /dev/null +++ b/stamina/settingtypes.txt @@ -0,0 +1,24 @@ +stamina.enabled (Is stamina enabled?) bool true +stamina.sprint (Is sprint enabled?) bool true +stamina.sprint_particles (Are sprint particles enabled?) bool true +stamina.tick (time in seconds after that 1 saturation point is taken) float 800 +stamina.tick_min (stamina ticks won't reduce saturation below this level) int 4 +stamina.health_tick (time in seconds after player gets healed/damaged) float 4 +stamina.move_tick (time in seconds after the movement is checked) float 0.5 +stamina.exhaust_dig (exhaustion for digging a node) float 3 +stamina.exhaust_place (exhaustion for placing a node) float 1 +stamina.exhaust_move (exhaustion for moving) float 1.5 +stamina.exhaust_jump (exhaustion for jumping) float 5 +stamina.exhaust_craft (exhaustion for crafting) float 20 +stamina.exhaust_punch (exhaustion for punching) float 40 +stamina.exhaust_sprint (exhaustion for sprinting) float 28 +stamina.exhaust_lvl (exhaustion level at which saturation gets lowered) float 160 +stamina.heal (amount of HP a player gains per stamina.health_tick) int 1 0 20 +stamina.heal_lvl (minimum saturation needed for healing) int 5 1 20 +stamina.starve (amount of HP a player loses per stamina.health_tick) int 1 0 20 +stamina.starve_lvl (maximum stamina needed for starving) int 3 0 19 +stamina.visual_max (hud bar only extends to 20) int 20 2 20 +stamina.sprint_speed (how much faster a player can run if satiated) float 0.8 0 2 +stamina.sprint_jump (how much faster a player can jump if satiated) float 0.1 0 2 +stamina.eat_particles (Whether to generate particles when eating) bool true +stamina.sprint_with_fast (Sprint when player has fast privilege?) bool false diff --git a/stamina/sounds/stamina_eat.1.ogg b/stamina/sounds/stamina_eat.1.ogg new file mode 100644 index 0000000..da2b68b Binary files /dev/null and b/stamina/sounds/stamina_eat.1.ogg differ diff --git a/stamina/sounds/stamina_eat.2.ogg b/stamina/sounds/stamina_eat.2.ogg new file mode 100644 index 0000000..edca423 Binary files /dev/null and b/stamina/sounds/stamina_eat.2.ogg differ diff --git a/stamina/sounds/stamina_eat.3.ogg b/stamina/sounds/stamina_eat.3.ogg new file mode 100644 index 0000000..1f06ea5 Binary files /dev/null and b/stamina/sounds/stamina_eat.3.ogg differ diff --git a/stamina/textures/stamina_hud_bg.png b/stamina/textures/stamina_hud_bg.png new file mode 100644 index 0000000..07e21e7 Binary files /dev/null and b/stamina/textures/stamina_hud_bg.png differ diff --git a/stamina/textures/stamina_hud_fg.png b/stamina/textures/stamina_hud_fg.png new file mode 100644 index 0000000..a5cc2a1 Binary files /dev/null and b/stamina/textures/stamina_hud_fg.png differ diff --git a/stamina/textures/stamina_hud_poison.png b/stamina/textures/stamina_hud_poison.png new file mode 100644 index 0000000..0f80ad2 Binary files /dev/null and b/stamina/textures/stamina_hud_poison.png differ diff --git a/towercrane/control.lua b/towercrane/control.lua index d766cf1..e4b594c 100644 --- a/towercrane/control.lua +++ b/towercrane/control.lua @@ -71,21 +71,27 @@ local function set_operator_privs(player, pos) local privs = minetest.get_player_privs(player:get_player_name()) local physics = player:get_physics_override() local meta = player:get_meta() - if pos and meta and privs and physics then - meta:set_string("towercrane_pos", P2S(pos)) - -- store the player privs default values - meta:set_string("towercrane_fast", privs["fast"] and "true" or "false") - meta:set_string("towercrane_fly", privs["fly"] and "true" or "false") - meta:set_int("towercrane_speed", physics.speed) - -- set operator privs - meta:set_int("towercrane_isoperator", 1) - privs["fly"] = true - privs["fast"] = nil - physics.speed = 0.7 - -- write back - player:set_physics_override(physics) - minetest.set_player_privs(player:get_player_name(), privs) + -- Check access conflicts with other mods + if meta:get_int("player_physics_locked") == 0 then + if pos and meta and privs and physics then + meta:set_string("towercrane_pos", P2S(pos)) + -- store the player privs default values + meta:set_string("towercrane_fast", privs["fast"] and "true" or "false") + meta:set_string("towercrane_fly", privs["fly"] and "true" or "false") + meta:set_int("towercrane_speed", physics.speed) + -- set operator privs + meta:set_int("towercrane_isoperator", 1) + meta:set_int("player_physics_locked", 1) + privs["fly"] = true + privs["fast"] = nil + physics.speed = 0.7 + -- write back + player:set_physics_override(physics) + minetest.set_player_privs(player:get_player_name(), privs) + return true + end end + return false end local function reset_operator_privs(player) @@ -96,6 +102,7 @@ local function reset_operator_privs(player) meta:set_string("towercrane_pos", "") -- restore the player privs default values meta:set_int("towercrane_isoperator", 0) + meta:set_int("player_physics_locked", 0) privs["fast"] = meta:get_string("towercrane_fast") == "true" or nil privs["fly"] = meta:get_string("towercrane_fly") == "true" or nil physics.speed = meta:get_int("towercrane_speed") @@ -275,10 +282,11 @@ minetest.register_node("towercrane:mast_ctrl_off", { on_rightclick = function (pos, node, clicker) if is_my_crane(pos, clicker) and not is_operator(clicker) then start_crane(pos, clicker) - set_operator_privs(clicker, pos) - local pos1, pos2 = calc_construction_area(pos) - -- control player every second - minetest.after(1, control_player, pos, pos1, pos2, clicker:get_player_name()) + if set_operator_privs(clicker, pos) then + local pos1, pos2 = calc_construction_area(pos) + -- control player every second + minetest.after(1, control_player, pos, pos1, pos2, clicker:get_player_name()) + end end end,