First version
commit
1be547fa72
|
@ -0,0 +1,45 @@
|
|||
# Respawn Timer (`respawn_timer`)
|
||||
|
||||
Forces players to wait a set duration before respawning.
|
||||
|
||||
## About
|
||||
|
||||
Depends on [`modlib`](https://github.com/appgurueu/modlib). Licensed under the MIT License. Written by Lars Mueller aka LMD or appguru(eu).
|
||||
|
||||
## Screenshot
|
||||
|
||||
## Links
|
||||
|
||||
* [GitHub](https://github.com/appgurueu/respawn_timer) - sources, issue tracking, contributing
|
||||
* [Discord](https://discordapp.com/invite/ysP74by) - discussion, chatting
|
||||
* [Minetest Forum](https://forum.minetest.net/viewtopic.php?f=9&t=<!--TODO -->) - (more organized) discussion
|
||||
* [ContentDB](https://content.minetest.net/packages/LMD/respawn_timer) - releases (cloning from GitHub is recommended)
|
||||
|
||||
## Features
|
||||
|
||||
* Player is actually dead while dead (`player:get_hp() == 0`). This minimizes breakage of other mods.
|
||||
* Custom respawn formspec (simple button).
|
||||
* Enhanced security: Actions of dead players (chatting, inventory, ...) are forbidden.
|
||||
* Basic persistence: Rejoining will only reset the timer.
|
||||
|
||||
## API
|
||||
|
||||
Relies on multiple hacks and thus imposes the following limitations on other mods:
|
||||
|
||||
* No manual modification of the `minetest.registered_on_dieplayers` table `on_mods_loaded`
|
||||
* No insertion at index 1 of the `minetest.registered_on_chat_messages` table `on_mods_loaded`
|
||||
* Mods doing this have to be listed as optional dependencies
|
||||
* Mods using bone position overrides on nonstandard bones have to use the API for those to be preserved properly
|
||||
* Standard bone names are `"Head"`, `"Body"`, `"Arm_Right"`, `"Arm_Left"`, `"Leg_Right"`, `"Leg_Left"`
|
||||
* `respawn_timer.bone_names_by_model["<filename>.<ext>"] = { "Bone_1", "Bone_2", ... }` including standard bone names if used
|
||||
* If both bone position and rotation are set to `{ x = 0, y = 0, z = 0 }`, the bone will be ignored
|
||||
|
||||
Respawning can be done using `respawn_timer.respawn(player_ref)`, the timer can be modified by altering the exposed table
|
||||
|
||||
```lua
|
||||
respawn_timer.timer = {
|
||||
name = "Respawn",
|
||||
duration = 5,
|
||||
color = "FF00FF"
|
||||
}
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
modlib.mod.init()
|
||||
-- Testing helpers, don't use in a live environment
|
||||
-- modlib.mod.extend"test"
|
|
@ -0,0 +1,222 @@
|
|||
local modname = minetest.get_current_modname()
|
||||
timer = {
|
||||
name = "Respawn",
|
||||
duration = 5,
|
||||
color = "FF00FF"
|
||||
}
|
||||
bone_names_by_model = {
|
||||
default = {"Head", "Body", "Arm_Right", "Arm_Left", "Leg_Right", "Leg_Left"}
|
||||
}
|
||||
local players = {}
|
||||
local respawn_formspec_name = modname .. ":respawn"
|
||||
respawn_formspec = "size[2,1]real_coordinates[true]button_exit[0,0;2,1;respawn;Respawn]"
|
||||
inventory_formspec_dead = "size[2,1]real_coordinates[true]label[0.25,0.5;You died]"
|
||||
local corpse = modname .. ":corpse"
|
||||
minetest.register_entity(corpse, {
|
||||
initial_properties = {
|
||||
physical = true,
|
||||
collide_with_objects = true,
|
||||
collisionbox = {-0.5, 0.0, -0.5, 0.5, 1.0, 0.5},
|
||||
static_save = false
|
||||
},
|
||||
on_activate = function(self)
|
||||
self.object:set_armor_groups{immortal = 1}
|
||||
self.object:set_acceleration{x = 0, y = -9.81, z = 0}
|
||||
end,
|
||||
on_step = function(self)
|
||||
if not self._player then
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
-- Copy player appearance
|
||||
local anim = {self.object:get_animation()}
|
||||
local player_anim = {self._player:get_animation()}
|
||||
if not modlib.table.equals(anim, player_anim) then
|
||||
self.object:set_animation(unpack(player_anim))
|
||||
end
|
||||
local bone_names = bone_names_by_model[self._player:get_properties().mesh] or bone_names_by_model.default
|
||||
for _, bone_name in pairs(bone_names) do
|
||||
local position, rotation = self._player:get_bone_position(bone_name)
|
||||
local corpse_pos, corpse_rot = self.object:get_bone_position(bone_name)
|
||||
if not (vector.equals(position, corpse_pos) and vector.equals(rotation, corpse_rot)) then
|
||||
self.object:set_bone_position(bone_name, position, rotation)
|
||||
end
|
||||
end
|
||||
self.object:set_properties{collisionbox = self._player.collisionbox}
|
||||
self._player:set_attach(self.object, "", vector.new(0, 0, 0), vector.new(0, 0, 0))
|
||||
self._player:set_pos(self.object:get_pos())
|
||||
end
|
||||
})
|
||||
minetest.register_on_mods_loaded(function()
|
||||
-- HACK to route all on_respawnplayer callbacks over respawn(player)
|
||||
local ignore_next_on_player_hpchange = false
|
||||
registered_on_respawnplayers = minetest.registered_on_respawnplayers
|
||||
function minetest.register_on_respawnplayer(func)
|
||||
assert(type(func) == "function")
|
||||
table.insert(registered_on_respawnplayers, func)
|
||||
end
|
||||
minetest.registered_on_respawnplayers = {}
|
||||
function respawn(player)
|
||||
local name = player:get_player_name()
|
||||
local data = assert(players[name])
|
||||
-- Detach player from corpse
|
||||
player:set_detach()
|
||||
-- Remove corpse
|
||||
data.corpse:remove()
|
||||
-- Reset player properties
|
||||
player:set_properties{hp_max = data.hp_max, visual_size = data.visual_size}
|
||||
-- Execute on_respawnplayer callbacks
|
||||
local reposition
|
||||
for _, callback in ipairs(registered_on_respawnplayers) do
|
||||
reposition = reposition or callback(player)
|
||||
end
|
||||
if reposition then
|
||||
player:set_pos(player:get_pos())
|
||||
end
|
||||
-- Explicitly unignore next HP change
|
||||
ignore_next_on_player_hpchange = false
|
||||
-- Do stuff Minetest does on respawn
|
||||
player:set_hp(data.hp_max, "respawn")
|
||||
player:set_breath(player:get_properties().breath_max)
|
||||
-- Inventory formspec & hud flags
|
||||
player:set_inventory_formspec(data.inventory_formspec)
|
||||
player:hud_set_flags(data.hud_flags)
|
||||
players[name] = nil
|
||||
end
|
||||
local function add_corpse(player, props)
|
||||
local obj = minetest.add_entity(player:get_pos(), corpse)
|
||||
-- Preserve most player properties
|
||||
props.pointable = false
|
||||
props.physical = true
|
||||
props.collide_with_objects = true
|
||||
props.backface_culling = true
|
||||
obj:set_properties(props)
|
||||
-- Preserve animation
|
||||
obj:set_animation(player:get_animation())
|
||||
obj:get_luaentity()._player = player
|
||||
player:set_attach(obj, "", vector.new(0, 0, 0), vector.new(0, 0, 0))
|
||||
obj:set_yaw(player:get_look_horizontal())
|
||||
players[player:get_player_name()].corpse = obj
|
||||
end
|
||||
local function add_timer(name)
|
||||
hud_timers.add_timer(name, {
|
||||
name = timer.name,
|
||||
duration = timer.duration,
|
||||
color = timer.color,
|
||||
on_complete = function()
|
||||
players[name].can_respawn = true
|
||||
minetest.show_formspec(name, respawn_formspec_name, respawn_formspec)
|
||||
end
|
||||
})
|
||||
end
|
||||
-- HACK allow revoking interact
|
||||
minetest.registered_privileges.interact.give_to_singleplayer = false
|
||||
function ghostify(player)
|
||||
local name = player:get_player_name()
|
||||
local player_props = player:get_properties()
|
||||
players[name] = {
|
||||
hp_max = player_props.hp_max,
|
||||
visual_size = player_props.visual_size,
|
||||
inventory_formspec = player:get_inventory_formspec(),
|
||||
hud_flags = player:hud_get_flags()
|
||||
}
|
||||
-- HACK to keep player dead
|
||||
add_corpse(player, player_props)
|
||||
ignore_next_on_player_hpchange = true
|
||||
player:set_properties{hp_max = 0, visual_size = {x = 0, y = 0, z = 0}}
|
||||
-- TODO revoke interact for better user experience at the expense of compatibility
|
||||
player:hud_set_flags{
|
||||
hotbar = false,
|
||||
healthbar = false,
|
||||
crosshair = false,
|
||||
wielditem = false,
|
||||
breathbar = false,
|
||||
minimap = false,
|
||||
minimap_radar = false
|
||||
}
|
||||
player:set_inventory_formspec(inventory_formspec_dead)
|
||||
-- HACK to close respawn formspec
|
||||
minetest.close_formspec(name, "")
|
||||
minetest.after(0.1, minetest.close_formspec, name, "")
|
||||
end
|
||||
-- HACK ghostify player on join
|
||||
table.insert(minetest.registered_on_joinplayers, 1, function(player)
|
||||
local name = player:get_player_name()
|
||||
if player:get_hp() == 0 then
|
||||
-- HACK ghostification has to happen AFTER the model has been set
|
||||
if player_api then
|
||||
player_api.player_attached[name] = false
|
||||
player_api.set_model(player, "character.b3d")
|
||||
end
|
||||
-- HACK ignore next on_dieplayer callback for player
|
||||
ignore_on_dieplayer[name] = true
|
||||
ghostify(player)
|
||||
end
|
||||
end)
|
||||
-- HACK add the timer AFTER the on_joinplayer callback by hud_timers has already executed
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
if player:get_hp() == 0 then
|
||||
add_timer(player:get_player_name())
|
||||
end
|
||||
end)
|
||||
-- HACK override internal Minetest callbackto "ghostify" player on death and to prevent double execution
|
||||
local original = minetest.registered_on_player_hpchange
|
||||
function minetest.registered_on_player_hpchange(player, hp_change, reason)
|
||||
if ignore_next_on_player_hpchange then
|
||||
ignore_next_on_player_hpchange = false
|
||||
return hp_change
|
||||
end
|
||||
hp_change = original(player, hp_change, reason)
|
||||
if player:get_hp() == 0 and hp_change < 0 then
|
||||
return 0
|
||||
end
|
||||
local new_hp = player:get_hp() + hp_change
|
||||
if new_hp <= 0 then
|
||||
ghostify(player)
|
||||
add_timer(player:get_player_name())
|
||||
return hp_change
|
||||
end
|
||||
return hp_change
|
||||
end
|
||||
-- HACK override registered_on_dieplayers to prevent double execution
|
||||
ignore_on_dieplayer = {}
|
||||
minetest.register_on_respawnplayer(function(player)
|
||||
ignore_on_dieplayer[player:get_player_name()] = nil
|
||||
end)
|
||||
registered_on_dieplayers = minetest.registered_on_dieplayers
|
||||
function minetest.register_on_dieplayer(func)
|
||||
assert(type(func) == "function")
|
||||
table.insert(registered_on_dieplayers, func)
|
||||
end
|
||||
minetest.registered_on_dieplayers = {function(player, ...)
|
||||
local name = player:get_player_name()
|
||||
if ignore_on_dieplayer[name] then
|
||||
return
|
||||
end
|
||||
for _, on_dieplayer in ipairs(registered_on_dieplayers) do
|
||||
on_dieplayer(player, ...)
|
||||
end
|
||||
ignore_on_dieplayer[name] = true
|
||||
end}
|
||||
-- TODO override nodemeta & detached inventory callbacks to also disallow such inventory actions
|
||||
minetest.register_allow_player_inventory_action(function(player)
|
||||
if player:get_hp() == 0 then
|
||||
return 0
|
||||
end
|
||||
end)
|
||||
-- Disable chat & commands
|
||||
table.insert(minetest.registered_on_chat_messages, 1, function(name)
|
||||
if minetest.get_player_by_name(name):get_hp() == 0 then
|
||||
minetest.chat_send_player(name, "You can't use commands or chat while dead!")
|
||||
return true
|
||||
end
|
||||
end)
|
||||
end)
|
||||
-- Respawning
|
||||
-- TODO provide alternative method in case this fails
|
||||
modlib.minetest.register_form_listener(respawn_formspec_name, function(player)
|
||||
local playerdata = players[player:get_player_name()]
|
||||
if playerdata and playerdata.can_respawn then
|
||||
respawn(player)
|
||||
end
|
||||
end)
|
|
@ -0,0 +1,4 @@
|
|||
name = respawn_timer
|
||||
description = Forces players to wait a set duration before respawning
|
||||
depends = hud_timers
|
||||
optional_depends = player_api, adv_chat, cmdlib
|
|
@ -0,0 +1,11 @@
|
|||
minetest.register_on_player_hpchange(function(player)
|
||||
minetest.chat_send_all("HP of player " .. player:get_player_name() .. " has changed")
|
||||
end)
|
||||
|
||||
minetest.register_on_dieplayer(function(player, reason)
|
||||
minetest.chat_send_all("Player " .. player:get_player_name() .. " died, reason: " .. minetest.write_json(reason))
|
||||
end)
|
||||
|
||||
minetest.register_on_respawnplayer(function(player)
|
||||
minetest.chat_send_all("Player " .. player:get_player_name() .. " respawned")
|
||||
end)
|
Loading…
Reference in New Issue