-- /// 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)