local CombatServer = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Debris = game:GetService("Debris")
local PhysicsService = game:GetService("PhysicsService")
local ServerScriptService = game:GetService("ServerScriptService")
local Config = require(ReplicatedStorage:WaitForChild("CombatConfig"))
local AIConfig = require(ReplicatedStorage:WaitForChild("AIConfig"))
local BlockingServer = require(ServerScriptService:WaitForChild("BlockingServer"))
local CombatRemote = ReplicatedStorage:FindFirstChild("CombatRemote")
if not CombatRemote then
CombatRemote = Instance.new("RemoteEvent")
CombatRemote.Name = "CombatRemote"
CombatRemote.Parent = ReplicatedStorage
end
local DashRemote = ReplicatedStorage:FindFirstChild("DashRemote")
if not DashRemote then
DashRemote = Instance.new("RemoteEvent")
DashRemote.Name = "DashRemote"
DashRemote.Parent = ReplicatedStorage
end
pcall(function()
PhysicsService:RegisterCollisionGroup("RagdollLimbs")
PhysicsService:RegisterCollisionGroup("RagdollTorso")
PhysicsService:CollisionGroupSetCollidable("RagdollLimbs", "RagdollLimbs", false)
PhysicsService:CollisionGroupSetCollidable("RagdollTorso", "RagdollLimbs", true)
end)
local PlayerDashCooldownTime = Config.DASH_COOLDOWN or 3
local GlobalHitCooldown = Config.GLOBAL_HIT_COOLDOWN or 0.5
local FinisherExtraCooldown = Config.FINISHER_EXTRA_COOLDOWN or 0.5
local RateLimitMaxHits = Config.RATE_LIMIT_MAX_HITS or 5
local RateLimitWindow = Config.RATE_LIMIT_WINDOW or 3
local PlayerDashCooldowns = {}
local PlayerStates = {}
type TargetData = {
Model: Model,
Humanoid: Humanoid,
RootPart: BasePart,
Distance: number,
}
type PlayerState = {
CurrentHit: number,
LastHitTime: number,
NextAllowedHit: number,
GlobalCooldownUntil: number,
Processing: boolean,
HitTimestamps: { number },
}
local function ApplyDash(Character: Model, Distance: number, Duration: number, Direction: Vector3?)
local RootPart = Character:FindFirstChild("HumanoidRootPart")
if not RootPart then return end
local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
if not Humanoid then return end
local MoveDirection = Direction or (RootPart :: BasePart).CFrame.LookVector
local Attachment = Instance.new("Attachment")
Attachment.Parent = RootPart
local LinearVelocity = Instance.new("LinearVelocity")
LinearVelocity.Attachment0 = Attachment
LinearVelocity.VelocityConstraintMode = Enum.VelocityConstraintMode.Vector
LinearVelocity.Vector = MoveDirection * (Distance / Duration)
LinearVelocity.MaxForce = Config.LINEAR_VELOCITY_MAX_FORCE or 1e5
LinearVelocity.RelativeTo = Enum.ActuatorRelativeTo.World
LinearVelocity.Parent = RootPart
Debris:AddItem(LinearVelocity, Duration)
Debris:AddItem(Attachment, Duration)
end
local function ApplyHitstun(Humanoid: Humanoid)
if not Humanoid or not Humanoid.Parent then return end
local CountKey = "HitstunCount"
local WalkSpeedKey = "NaturalWalkSpeed"
local JumpPowerKey = "NaturalJumpPower"
local JumpHeightKey = "NaturalJumpHeight"
local Count = (Humanoid:GetAttribute(CountKey) or 0) + 1
Humanoid:SetAttribute(CountKey, Count)
if Count == 1 then
if not Humanoid:GetAttribute(WalkSpeedKey) then
Humanoid:SetAttribute(WalkSpeedKey, Config.WALKSPEED_NORMAL)
end
if not Humanoid:GetAttribute(JumpPowerKey) then
Humanoid:SetAttribute(JumpPowerKey, Humanoid.JumpPower > 5 and Humanoid.JumpPower or 50)
end
if not Humanoid:GetAttribute(JumpHeightKey) then
Humanoid:SetAttribute(JumpHeightKey, Humanoid.JumpHeight > 1 and Humanoid.JumpHeight or 7.2)
end
end
Humanoid.WalkSpeed = Config.HITSTUN_WALKSPEED or 2
Humanoid.JumpPower = Config.HITSTUN_JUMPPOWER or 0
Humanoid.JumpHeight = Config.HITSTUN_JUMPHEIGHT or 0
task.delay(Config.HITSTUN_DURATION, function()
if not Humanoid or not Humanoid.Parent then return end
local Current = math.max(0, (Humanoid:GetAttribute(CountKey) or 1) - 1)
Humanoid:SetAttribute(CountKey, Current)
if Current == 0 then
local WalkSpeed = Humanoid:GetAttribute(WalkSpeedKey) or Config.WALKSPEED_NORMAL
local JumpPower = Humanoid:GetAttribute(JumpPowerKey) or 50
local JumpHeight = Humanoid:GetAttribute(JumpHeightKey) or 7.2
Humanoid.WalkSpeed = WalkSpeed
Humanoid.JumpPower = JumpPower
Humanoid.JumpHeight = JumpHeight
end
end)
end
local function ApplyKnockback(TargetRoot: BasePart, Direction: Vector3, Strength: number, UpForce: number?)
local HorizontalDir = Vector3.new(Direction.X, 0, Direction.Z)
if HorizontalDir.Magnitude < 0.01 then
HorizontalDir = Vector3.new(0, 0, 1)
end
local Attachment = Instance.new("Attachment")
Attachment.Parent = TargetRoot
local LinearVelocity = Instance.new("LinearVelocity")
LinearVelocity.Attachment0 = Attachment
LinearVelocity.VelocityConstraintMode = Enum.VelocityConstraintMode.Vector
LinearVelocity.Vector = HorizontalDir.Unit * Strength + Vector3.new(0, UpForce or Config.KNOCKBACK_DEFAULT_UPFORCE or 0.25, 0)
LinearVelocity.MaxForce = Config.LINEAR_VELOCITY_MAX_FORCE or 1e5
LinearVelocity.RelativeTo = Enum.ActuatorRelativeTo.World
LinearVelocity.Parent = TargetRoot
Debris:AddItem(LinearVelocity, Config.KNOCKBACK_DEBRIS_TIME or 0.22)
Debris:AddItem(Attachment, Config.KNOCKBACK_DEBRIS_TIME or 0.22)
end
local function RagdollCharacter(Character: Model, Duration: number)
local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
if not Humanoid then return end
local RagdollKey = Config.RAGDOLL_COUNT_ATTRIBUTE or "RagdollCount"
local IsRagdolledKey = Config.RAGDOLL_IS_RAGDOLLED_ATTRIBUTE or "IsRagdolled"
local Count = (Humanoid:GetAttribute(RagdollKey) or 0) + 1
Humanoid:SetAttribute(RagdollKey, Count)
Humanoid:SetAttribute(IsRagdolledKey, true)
if Count == 1 then
local Animator = Humanoid:FindFirstChildWhichIsA("Animator")
if Animator then
for _, Track in ipairs(Animator:GetPlayingAnimationTracks()) do
Track:Stop(0)
end
end
Humanoid.PlatformStand = true
Humanoid:ChangeState(Enum.HumanoidStateType.Physics)
for _, Motor in ipairs(Character:GetDescendants()) do
if Motor:IsA("Motor6D") and Motor.Name ~= "Root" then
local ConnectedPart = Motor.Part1
if ConnectedPart then
Motor.Enabled = false
local JointName = Config.RAGDOLL_JOINT_NAME or "Ragdoll_Joint"
if not ConnectedPart:FindFirstChild(JointName) then
local Attachment0 = Motor.Part0:FindFirstChild(Motor.Name .. "RigAttachment")
local Attachment1 = Motor.Part1:FindFirstChild(Motor.Name .. "RigAttachment")
if Attachment0 and Attachment1 then
local BallSocket = Instance.new("BallSocketConstraint")
BallSocket.Name = JointName
BallSocket.Attachment0 = Attachment0 :: Attachment
BallSocket.Attachment1 = Attachment1 :: Attachment
BallSocket.Parent = ConnectedPart
end
end
end
end
end
for _, Part in ipairs(Character:GetDescendants()) do
if Part:IsA("BodyGyro") or Part:IsA("BodyAngularVelocity") or Part:IsA("AlignOrientation") then
Part:Destroy()
end
if Part:IsA("BasePart") then
Part.Anchored = false
Part.CanCollide = Part.Name ~= "HumanoidRootPart"
if not Players:GetPlayerFromCharacter(Character) then
pcall(function()
Part:SetNetworkOwner(nil)
end)
end
end
end
local RootPart = Character:FindFirstChild("HumanoidRootPart")
if RootPart and RootPart:IsA("BasePart") then
local MinAng = Config.RAGDOLL_ANGULAR_MIN or 20
local MaxAng = Config.RAGDOLL_ANGULAR_MAX or 50
local Impulse = Config.RAGDOLL_IMPULSE or 50
RootPart.AssemblyAngularVelocity = Vector3.new(math.random(MinAng, MaxAng), 0, math.random(MinAng, MaxAng))
RootPart:ApplyImpulse(Vector3.new(0, Impulse, 0))
end
end
task.delay(Duration, function()
if not Humanoid or not Humanoid.Parent then return end
local Current = math.max(0, (Humanoid:GetAttribute(RagdollKey) or 1) - 1)
Humanoid:SetAttribute(RagdollKey, Current)
if Current == 0 then
for _, Motor in ipairs(Character:GetDescendants()) do
if Motor:IsA("Motor6D") then
Motor.Enabled = true
end
end
local JointName = Config.RAGDOLL_JOINT_NAME or "Ragdoll_Joint"
for _, Item in ipairs(Character:GetDescendants()) do
if Item.Name == JointName then
Item:Destroy()
end
end
local RootPart = Character:FindFirstChild("HumanoidRootPart")
if RootPart and RootPart:IsA("BasePart") then
RootPart.CanCollide = true
end
Humanoid.PlatformStand = false
Humanoid:SetAttribute(IsRagdolledKey, false)
Humanoid:ChangeState(Enum.HumanoidStateType.GettingUp)
end
end)
end
local function TriggerRagdoll(Character: Model, Duration: number)
local TargetPlayer = Players:GetPlayerFromCharacter(Character)
if TargetPlayer then
CombatRemote:FireClient(TargetPlayer, "ApplyRagdoll", Duration)
end
RagdollCharacter(Character, Duration)
end
local function CreateHitbox(Character: Model, HitNumber: number, SizeOverride: Vector3?, OffsetOverride: CFrame?): Part?
local RootPart = Character:FindFirstChild("HumanoidRootPart")
if not RootPart or not RootPart:IsA("BasePart") then return nil end
local Hitbox = Instance.new("Part")
Hitbox.Anchored = true
Hitbox.CanCollide = false
Hitbox.CanTouch = false
Hitbox.CanQuery = true
Hitbox.Transparency = 1
Hitbox.Size = SizeOverride
or (HitNumber == 3 and Config.HITBOX_SIZE_HIT3 or Config.HITBOX_SIZE_HIT12)
local Offset = OffsetOverride
or (HitNumber == 3 and Config.HITBOX_OFFSET_HIT3 or Config.HITBOX_OFFSET_HIT12)
Hitbox.CFrame = (RootPart :: BasePart).CFrame * Offset
Hitbox.Parent = workspace
return Hitbox
end
local function GetTargets(AttackerCharacter: Model, Hitbox: Part, _HitNumber: number, ArcOverride: number?, MaxTargets: number?): { TargetData }
local RootPart = AttackerCharacter:FindFirstChild("HumanoidRootPart")
if not RootPart or not RootPart:IsA("BasePart") then return {} end
local Arc = ArcOverride ~= nil and ArcOverride or Config.FRONT_ARC_DOT
local Limit = MaxTargets or Config.MAX_TARGETS_PER_HIT or 3
local QueryParams = OverlapParams.new()
QueryParams.FilterType = Enum.RaycastFilterType.Exclude
QueryParams.FilterDescendantsInstances = { AttackerCharacter }
QueryParams.MaxParts = 30
local Parts = workspace:GetPartsInPart(Hitbox, QueryParams)
local Seen: { [Model]: boolean } = {}
local Results: { TargetData } = {}
local AttackerHumanoid = AttackerCharacter:FindFirstChildWhichIsA("Humanoid")
local AttackerIsPlayer = Players:GetPlayerFromCharacter(AttackerCharacter) ~= nil
for _, Part in ipairs(Parts) do
local FoundModel = Part:FindFirstAncestorOfClass("Model")
if FoundModel and not Seen[FoundModel] then
Seen[FoundModel] = true
local Humanoid = FoundModel:FindFirstChildWhichIsA("Humanoid")
local TargetRoot = FoundModel:FindFirstChild("HumanoidRootPart")
if Humanoid and Humanoid ~= AttackerHumanoid and Humanoid.Health > 0 and TargetRoot and TargetRoot:IsA("BasePart") then
local TargetIsPlayer = Players:GetPlayerFromCharacter(FoundModel) ~= nil
if AttackerIsPlayer or TargetIsPlayer then
local ToTarget = TargetRoot.Position - (RootPart :: BasePart).Position
local Distance = ToTarget.Magnitude
local InArc = Arc <= -1
or (Distance > 0.01 and (RootPart :: BasePart).CFrame.LookVector:Dot(ToTarget.Unit) > Arc)
if InArc then
table.insert(Results, {
Model = FoundModel,
Humanoid = Humanoid,
RootPart = TargetRoot :: BasePart,
Distance = Distance,
})
end
end
end
end
end
table.sort(Results, function(A, B)
return A.Distance < B.Distance
end)
if #Results > Limit then
local LimitedResults = {}
for i = 1, Limit do
LimitedResults[i] = Results[i]
end
return LimitedResults
end
return Results
end
local function GetHitStats(HitNumber: number)
if HitNumber == 1 then
return Config.DAMAGE_HIT1, Config.KNOCKBACK_HIT12
elseif HitNumber == 2 then
return Config.DAMAGE_HIT2, Config.KNOCKBACK_HIT12
else
return Config.DAMAGE_HIT3, Config.KNOCKBACK_HIT3
end
end
local function ApplyHitEffect(AttackerRoot: BasePart, Target: TargetData, HitNumber: number, Damage: number, Knockback: number, IsBlocked: boolean, KnockbackMultiplier: number)
local Direction = Target.RootPart.Position - AttackerRoot.Position
Direction = Vector3.new(Direction.X, 0, Direction.Z)
if Direction.Magnitude < 0.01 then
Direction = AttackerRoot.CFrame.LookVector
end
Direction = Direction.Unit
if IsBlocked then
local AdjustedKnockback = Knockback * KnockbackMultiplier
if HitNumber == 3 then
ApplyKnockback(Target.RootPart, Direction, AdjustedKnockback, Config.BLOCK_KNOCKBACK_UPFORCE_HIT3 or 0.35)
else
ApplyKnockback(Target.RootPart, Direction, AdjustedKnockback, Config.BLOCK_KNOCKBACK_UPFORCE_HIT12 or 0.08)
ApplyHitstun(Target.Humanoid)
end
return
end
local CreatorTag = Target.Humanoid:FindFirstChild("Creator")
if not CreatorTag then
CreatorTag = Instance.new("ObjectValue")
CreatorTag.Name = "Creator"
CreatorTag.Parent = Target.Humanoid
end
local AttackingPlayer = Players:GetPlayerFromCharacter(AttackerRoot.Parent)
if AttackingPlayer and CreatorTag:IsA("ObjectValue") then
CreatorTag.Value = AttackingPlayer
end
Target.Humanoid:TakeDamage(Damage)
if HitNumber == 3 then
ApplyKnockback(Target.RootPart, Direction, Knockback, 1.0)
TriggerRagdoll(Target.Model, Config.RAGDOLL_DURATION or 2.5)
else
ApplyKnockback(Target.RootPart, Direction, Knockback, 0.25)
ApplyHitstun(Target.Humanoid)
end
end
local function CheckRateLimit(State: PlayerState, Now: number): boolean
local WindowStart = Now - RateLimitWindow
local Write = 1
for i = 1, #State.HitTimestamps do
if State.HitTimestamps[i] >= WindowStart then
State.HitTimestamps[Write] = State.HitTimestamps[i]
Write += 1
end
end
for i = Write, #State.HitTimestamps do
State.HitTimestamps[i] = nil
end
if #State.HitTimestamps >= RateLimitMaxHits then return false end
table.insert(State.HitTimestamps, Now)
return true
end
local function RejectHit(Player: Player, Reason: string)
CombatRemote:FireClient(Player, "HitRejected", Reason)
end
local function ProcessHit(Player: Player, HitNumber: number)
local State = PlayerStates[Player] :: PlayerState
if not State then RejectHit(Player, "no_state") return end
local Now = os.clock()
if Now < State.GlobalCooldownUntil then RejectHit(Player, "global_cooldown") return end
if not CheckRateLimit(State, Now) then RejectHit(Player, "rate_limited") return end
local AnimationCooldown = HitNumber == 3 and Config.RECOVERY_HIT3 or Config.RECOVERY_HIT12
if Now < State.NextAllowedHit then RejectHit(Player, "animation_cooldown") return end
if State.Processing then RejectHit(Player, "busy") return end
local ExpectedHit = State.CurrentHit + 1
if ExpectedHit > (Config.COMBO_LENGTH or 3) then ExpectedHit = 1 end
if HitNumber ~= ExpectedHit then
State.CurrentHit = 0
RejectHit(Player, "wrong_order")
return
end
if State.CurrentHit > 0 and Now - State.LastHitTime > Config.COMBO_WINDOW then
State.CurrentHit = 0
RejectHit(Player, "window_expired")
return
end
local Character = Player.Character
if not Character then RejectHit(Player, "no_character") return end
local AttackerHumanoid = Character:FindFirstChildWhichIsA("Humanoid")
local IsRagdolledKey = Config.RAGDOLL_IS_RAGDOLLED_ATTRIBUTE or "IsRagdolled"
if AttackerHumanoid and AttackerHumanoid:GetAttribute(IsRagdolledKey) then
RejectHit(Player, "ragdolled")
return
end
State.Processing = true
local Targets: { TargetData } = {}
local Hitbox = CreateHitbox(Character, HitNumber)
if Hitbox then
local Ok = pcall(function()
Targets = GetTargets(Character, Hitbox, HitNumber, nil)
end)
Hitbox:Destroy()
if not Ok then
State.Processing = false
return
end
end
State.Processing = false
local TotalCooldown = math.max(AnimationCooldown, GlobalHitCooldown)
+ (HitNumber == 3 and FinisherExtraCooldown or 0)
State.NextAllowedHit = Now + TotalCooldown
State.GlobalCooldownUntil = Now + GlobalHitCooldown
local Damage, Knockback = GetHitStats(HitNumber)
local HitData: { { TargetRoot: BasePart, Damage: number } } = {}
local AttackerRoot = Character:FindFirstChild("HumanoidRootPart")
if not AttackerRoot or not AttackerRoot:IsA("BasePart") then return end
for _, Target in ipairs(Targets) do
if Target.Humanoid.Health > 0 then
local TargetPlayer = Players:GetPlayerFromCharacter(Target.Model)
local IsBlocked = false
local KnockbackMultiplier = 1.0
if TargetPlayer then
IsBlocked, KnockbackMultiplier = BlockingServer.checkBlock(Player, TargetPlayer, Target.Model)
if IsBlocked then BlockingServer.sendBlockedFeedback(TargetPlayer) end
else
IsBlocked, KnockbackMultiplier = BlockingServer.isCharacterBlockingAttacker(Character, Target.Model)
end
ApplyHitEffect(AttackerRoot :: BasePart, Target, HitNumber, Damage, Knockback, IsBlocked, KnockbackMultiplier)
if not IsBlocked then
table.insert(HitData, { TargetRoot = Target.RootPart, Damage = Damage })
end
end
end
if Config.DASH_ENABLED then
for _, TriggerHit in ipairs(Config.DASH_TRIGGER_HITS) do
if TriggerHit == HitNumber then
ApplyDash(Character, Config.DASH_DISTANCE, Config.DASH_DURATION)
break
end
end
end
State.LastHitTime = Now
State.CurrentHit = HitNumber == (Config.COMBO_LENGTH or 3) and 0 or HitNumber
if #HitData > 0 then
CombatRemote:FireClient(Player, "HitConfirmed", HitNumber, HitData)
else
CombatRemote:FireClient(Player, "HitMissed", HitNumber)
end
end
DashRemote.OnServerEvent:Connect(function(Player: Player)
local Now = os.clock()
local CooldownUntil = PlayerDashCooldowns[Player] or 0
local Remaining = CooldownUntil - Now
if Remaining > 0 then
DashRemote:FireClient(Player, "DashRejected", Remaining)
return
end
local Character = Player.Character
if not Character then
DashRemote:FireClient(Player, "DashRejected", 0)
return
end
local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
if not Humanoid or Humanoid.Health <= 0 then
DashRemote:FireClient(Player, "DashRejected", 0)
return
end
local IsRagdolledKey = Config.RAGDOLL_IS_RAGDOLLED_ATTRIBUTE or "IsRagdolled"
if Humanoid:GetAttribute(IsRagdolledKey) then
DashRemote:FireClient(Player, "DashRejected", 0)
return
end
local DashDistance = AIConfig.DashDistance or 15
local DashDuration = AIConfig.DashDuration or 0.3
ApplyDash(Character, DashDistance, DashDuration)
PlayerDashCooldowns[Player] = Now + PlayerDashCooldownTime
DashRemote:FireClient(Player, "DashOk")
end)
function CombatServer.PerformDummyPunch(DummyCharacter: Model, HitNumber: number, SizeOverride: Vector3?, OffsetOverride: CFrame?)
local RootPart = DummyCharacter:FindFirstChild("HumanoidRootPart")
if not RootPart or not RootPart:IsA("BasePart") then return end
local Humanoid = DummyCharacter:FindFirstChildWhichIsA("Humanoid")
if not Humanoid or Humanoid.Health <= 0 then return end
local IsRagdolledKey = Config.RAGDOLL_IS_RAGDOLLED_ATTRIBUTE or "IsRagdolled"
if Humanoid:GetAttribute(IsRagdolledKey) then return end
local Hitbox = CreateHitbox(DummyCharacter, HitNumber, SizeOverride, OffsetOverride)
if not Hitbox then return end
local Targets
local Ok = pcall(function()
Targets = GetTargets(DummyCharacter, Hitbox, HitNumber, -1)
end)
Hitbox:Destroy()
if not Ok or not Targets then return end
local Damage, Knockback = GetHitStats(HitNumber)
local HitData: { { TargetRoot: BasePart, Damage: number } } = {}
for _, Target in ipairs(Targets) do
if Target.Humanoid.Health > 0 then
local TargetPlayer = Players:GetPlayerFromCharacter(Target.Model)
local IsBlocked = false
local KnockbackMultiplier = 1.0
if TargetPlayer then
IsBlocked, KnockbackMultiplier = BlockingServer.checkBlockByCharacter(DummyCharacter, TargetPlayer, Target.Model)
if IsBlocked then BlockingServer.sendBlockedFeedback(TargetPlayer) end
else
IsBlocked, KnockbackMultiplier = BlockingServer.isCharacterBlockingAttacker(DummyCharacter, Target.Model)
end
ApplyHitEffect(RootPart :: BasePart, Target, HitNumber, Damage, Knockback, IsBlocked, KnockbackMultiplier)
if not IsBlocked then
table.insert(HitData, { TargetRoot = Target.RootPart, Damage = Damage })
end
end
end
return HitData
end
CombatServer.ApplyDash = ApplyDash
local function NewPlayerState(): PlayerState
return {
CurrentHit = 0,
LastHitTime = 0,
NextAllowedHit = 0,
GlobalCooldownUntil = 0,
Processing = false,
HitTimestamps = {},
}
end
Players.PlayerAdded:Connect(function(Player: Player)
PlayerStates[Player] = NewPlayerState()
PlayerDashCooldowns[Player] = 0
Player.CharacterAdded:Connect(function()
PlayerStates[Player] = NewPlayerState()
PlayerDashCooldowns[Player] = 0
end)
end)
Players.PlayerRemoving:Connect(function(Player: Player)
PlayerStates[Player] = nil
PlayerDashCooldowns[Player] = nil
end)
for _, Player in ipairs(Players:GetPlayers()) do
PlayerStates[Player] = NewPlayerState()
PlayerDashCooldowns[Player] = 0
end
CombatRemote.OnServerEvent:Connect(function(Player: Player, Action: string, HitNumber: number)
if Action == "RequestHit" and type(HitNumber) == "number" then
ProcessHit(Player, math.clamp(math.floor(HitNumber), 1, 3))
elseif Action == "ResetCombo" then
if PlayerStates[Player] then
PlayerStates[Player].CurrentHit = 0
end
end
end)
return CombatServer