Health System - table empty? Improvements?

-- /// CODE /// --
local HealthHandler = {}
HealthHandler.DownedPlayers = {}

-- // HELPER FUNCTIONS
--[[
Checks if the value does not go lower than player's HP
]]
local function LegalHealthOp(humanoid: Humanoid, amt: number) : boolean
	if humanoid and amt then
		local calc = humanoid.Health - amt
		return calc > 0
	else
		error("Humanoid and amount were not provided for helper function HealthCheck().", 2)
	end
end

--[[
Prints the message of defined strength if DebugMode is enabled.
]]
local function Log(message: string, strength: number)
	if Config.DebugMode then
		assert(message, Config.HHNDLRMODULE_PREFIX.."LOG >> Message not provided.")
		local logMessage = Config.HHNDLRMODULE_PREFIX..message
		local logMethods = {
			[1] = print,
			[2] = warn,
			[3] = function(msg) error(msg, 1) end
		}

		logMethods[strength or 1](logMessage)
	end
end

--[[
Generates a fallback for an event if specified event does not exist.
]]
local function GenerateFallbackEvent(service: Instance, name: string) : BindableEvent
	assert(service, "Service must be provided to GenerateFallbackEvent().")
	assert(name, "Event name must be provided to GenerateFallbackEvent().")

	local newEvent = Instance.new("BindableEvent")
	newEvent.Name = name
	newEvent.Parent = service

	Log("Fallback event: "..name.." created in "..service:GetFullName())
	return newEvent :: BindableEvent
end

--[[
Checks target service for required events
]]
local function GetEvent(service: Instance, name: string) : BindableEvent?
	assert(service, "Service must be provided to GetEvent().")
	assert(name, "Name must be provided to GetEvent().")
	local event = service:FindFirstChild(name, true)
	if not event then
		Log("GetEvent() could not fetch function of name "..name.." in "..service:GetFullName(), 2)
		event = GenerateFallbackEvent(service, name)
	end
	return event :: BindableEvent
end

--[[
Returns whether or not the player is downed.
]]
function HealthHandler:IsPlayerDowned(character: Model)
	return self.DownedPlayers[character] ~= nil
end


-- Events
local OverkillEvent = GetEvent(ServerStorage, "Overkill")
local OverhealEvent = GetEvent(ServerStorage, "Overheal")
local DownedEvent = GetEvent(ServerStorage, "Downed")
local RevivedEvent = GetEvent(ServerStorage, "Revived")

--[[
Helper function for handling overheal.
]]
local function HandleOverheal(humanoid: Humanoid, amount: number)
	local finalHealth = humanoid.Health + amount
	local healthOverhealed = finalHealth - humanoid.MaxHealth

	if healthOverhealed > 0 then
		OverhealEvent:Fire(humanoid.Parent, healthOverhealed) -- Assume humanoid.Parent is the character model
	end
end

--[[
Helper function for retrieving the humanoid.
]]
local function GetHumanoidOrLog(character: Model): Humanoid?
	local Humanoid = character:FindFirstChild("Humanoid") :: Humanoid
	if not Humanoid then
		Log("Failed to retrieve humanoid from character: "..character.Name, 2)
		return nil
	end
	return Humanoid
end

-- [[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]] --

-- /// MAIN CODE /// ---
--[[
Returns the entire table of downed players.
]]
function HealthHandler:RetrieveDownedTable() : {Model}
	return table.clone(self.DownedPlayers)
end

function HealthHandler:TickDown(character: Model, delta: number)
	local downedData = self.DownedPlayers[character]
	if downedData then
		downedData.TimeRemaining -= delta
		if downedData.TimeRemaining <= 0 then
			self:Kill(character)
		end
	end
end


--[[
Kills the player immediately. No checks, just instant death.
]]
function HealthHandler:Kill(character: Model)
	assert(character, "Character was not defined for HealthHandler:Kill()") -- Error if no character found
	local Humanoid = GetHumanoidOrLog(character)
	if Humanoid then
		Humanoid.Health = 0
		if self.DownedPlayers[character] then
			self.DownedPlayers[character] = nil
		end
	end
end


--[[
Revives the player if they are downed.
]]
function HealthHandler:Revive(character: Model)
	assert(character, "Character was not defined for HealthHandler:Revive()") -- Error if no character found
	local Humanoid = GetHumanoidOrLog(character)
	if Humanoid then
		if HealthHandler:IsPlayerDowned(character) then
			RagdollModule:Unragdoll(character)
			if RevivedEvent then RevivedEvent:Fire(character, self.DownedPlayers[character].TimeRemaining) end
			self.DownedPlayers[character] = nil
			Humanoid.Health = Humanoid.MaxHealth / 2
		end
	end
end

--[[
Immediately down the player, without taking into account health.
Cause - provide the cause, which will be specified as the second parameter of the Downed event, and stored in the downed players module.
]]
function HealthHandler:Down(character: Model, cause: string?)
	assert(character, "Character was not defined for HealthHandler:Down()")
	local Humanoid = GetHumanoidOrLog(character)
	if Humanoid then
		if not self:IsPlayerDowned(character) then
			RagdollModule:Ragdoll(character)
			self.DownedPlayers[character] = {
				TimeRemaining = Config.givenDownTime,
				DownedAt = tick(),
				InitialHealth =  Humanoid.Health,
				Cause = cause or "unspecified"
			}
			Humanoid.Health = math.max(Config.downThreshold, 0)
			if DownedEvent then DownedEvent:Fire(character, cause) end
			
			Log(character.Name.." was downed! Cause: "..(cause or "unspecified"), 2)
		end
	end
end

--[[
Deal damage to the character.
If overkill is allowed, if the damage exceeds a threshold - immediately kill the player.
If downing is allowed, down the player if their health drops below a threshold.
Additionally, takes into account whether the player's health drops below or to 0, in which case it has its own edgecases.
]]
function HealthHandler:TakeDamage(character: Model, amount: number)
	assert(character, "Character was not defined for HealthHandler:TakeDamage()") -- Error if no character found
	if not (amount > 0) then
		Log("Invalid damage amount: "..tostring(amount), 3)
		return
	end

	local Humanoid = GetHumanoidOrLog(character)
	if not Humanoid then Log("Could not find humanoid in HealthHandler:TakeDamage().", 2) return end

	local isDowned = self:IsPlayerDowned(character)
	local currentHealth = Humanoid.Health
	local postDamageHealth = currentHealth - amount
	local killThreshold = Humanoid.MaxHealth + Config.overkillThreshold

	-- Handle overkill
	if Config.overkillAllowed and amount >= killThreshold then
		if OverkillEvent then OverkillEvent:Fire(character) end
		Humanoid.Health = 0
		Log(character.Name.." was overkilled!", 2)
		return
	end

	-- Handle downed players' timer reduction
	if isDowned then
		self.DownedPlayers[character].TimeRemaining = math.max(
			self.DownedPlayers[character].TimeRemaining - amount / 0.1, 
			0
		)
		return
	end

	-- Handle regular damage
	if Config.downAllowed then
		-- Down the player if health drops below the threshold
		if postDamageHealth <= Config.downThreshold then
			self:Down(character)
			Log(character.Name.." has been downed!", 2)
			return
		end
	end

	-- Apply damage if health remains above 0
	if LegalHealthOp(Humanoid, amount) then
		Humanoid:TakeDamage(amount)
	else
		Humanoid.Health = 0
	end
end


--[[
Heal the character.
If the health is in the negatives, it will error.
If the health healed is more than max health of the player, it will invoke the Overheal event.
OVERHEAL PARAMS: character, healed amount
]]
function HealthHandler:Heal(character: Model, amount: number)
	assert(character, "Character was not defined for HealthHandler:Heal()") -- Error if no character found
	local Humanoid = GetHumanoidOrLog(character)
	if not (amount > 0) then -- If health is in the negatives.
		Log("Invalid heal amount: "..tostring(amount), 3)
		return
	end

	if Humanoid then 
		
		if Humanoid.Health <= 0 then
			Log("Cannot heal a dead player.", 2)
			return
		end
		
		-- Overheal
		if Humanoid.Health + amount > Humanoid.MaxHealth then -- Handle overheal
			HandleOverheal(Humanoid, amount)
		end
		
		-- Healing
		if self:IsPlayerDowned(character) then -- If player is downed, add some time
			self.DownedPlayers[character].TimeRemaining = math.min(
				self.DownedPlayers[character].TimeRemaining + amount / 0.1,
				Config.givenDownTime
			)
		else -- Else, heal them normally
			Humanoid.Health += amount
		end
	end
end

return HealthHandler

Hello! I’ve been recently making a custom health system I thought of releasing for the whole wide world, but here’s the thing:
The DownedPlayers table returns as empty for some reason.

I took myself down via a script, which should’ve added my character as a key to the table. However, when printing said table out in the command bar, it was an empty table for whatever reason?

I do not get the issue.

Additionally, I would appreciate feedback on the remainder of my code, as well as suggestions for how to implement a timer until the player dies (that the player could also see). Thank you!

EXTRA, TIMER CODE:

-- // SERVICES
local RunService = game:GetService("RunService")
local RepStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

-- // FOLDER
local folder = script.Parent
local eventFolder = RepStorage.Events.RemoteEvents
local Module = require(folder.HealthHandler)

local function OnHeartbeat(deltaTime)
	if not Module.DownedPlayers[1] then return end
	for character, data in pairs(Module.DownedPlayers) do
		local timeRemaining = data.TimeRemaining
		Module:TickDown(character, deltaTime)
		if character:FindFirstChild("Player") then
			local player = Players:GetPlayerFromCharacter(character)
			eventFolder.UpdateDownedGUI:FireClient(player, timeRemaining)
		end
	end
	task.wait(1)
end

RunService.Heartbeat:Connect(OnHeartbeat)

(additionally, if you are a random scroller, I ask not to take my code until it is released. I would appreciate the satisfaction of helping people, thank you)

1 Like

Bumping this (sorry, but I really need help)

The command bar runs in a different environment and has its own moduleScript, not the one which the server has.