🎯

combat-system

🎯Skill

from taozhuo/game-dev-skills

VibeIndex|
What it does

Enables comprehensive combat mechanics with advanced hitbox detection, damage calculation, and hit validation for creating dynamic player-vs-player and player-vs-NPC combat interactions.

combat-system

Installation

Install skill:
npx skills add https://github.com/taozhuo/game-dev-skills --skill combat-system
1
AddedJan 27, 2026

Skill Details

SKILL.md

Implements combat systems including hitboxes, damage calculation, stun mechanics, melee combat, ranged weapons, and ability systems. Use when building fighting games, shooters, RPG combat, or any game with player-vs-player or player-vs-NPC combat.

Overview

# Roblox Combat System Implementation

When implementing combat systems, follow these Roblox-specific patterns.

Hitbox Systems

Battlegrounds-Style Hitbox (GetPartBoundsInBox)

```lua

-- Most common method for melee games like The Strongest Battlegrounds

local function createHitbox(cframe, size, ignoreList, callback)

local params = OverlapParams.new()

params.FilterDescendantsInstances = ignoreList

params.FilterType = Enum.RaycastFilterType.Exclude

local parts = workspace:GetPartBoundsInBox(cframe, size, params)

local hitCharacters = {}

for _, part in ipairs(parts) do

local character = part.Parent

local humanoid = character:FindFirstChildOfClass("Humanoid")

if humanoid and not hitCharacters[character] then

hitCharacters[character] = true

callback(humanoid, part)

end

end

return hitCharacters

end

-- Usage with lingering hitbox (active for multiple frames)

local function meleeAttack(attacker, damage, knockback)

local hitTargets = {}

local duration = 0.2 -- Hitbox active for 200ms

local startTime = os.clock()

while os.clock() - startTime < duration do

local hitboxCFrame = attacker.HumanoidRootPart.CFrame * CFrame.new(0, 0, -3)

local hitboxSize = Vector3.new(4, 6, 4)

createHitbox(hitboxCFrame, hitboxSize, {attacker}, function(humanoid, hitPart)

if not hitTargets[humanoid.Parent] then

hitTargets[humanoid.Parent] = true

applyDamage(humanoid, damage, knockback, attacker)

end

end)

task.wait()

end

end

```

Client-Predicted Hitbox (Validate on Server)

```lua

-- Client: Perform hit detection locally for responsiveness

local function clientHitDetection(attackData)

local hitbox = createHitbox(attackData.cframe, attackData.size, {localPlayer.Character})

for character, _ in pairs(hitbox) do

-- Send hit claim to server

CombatRemote:FireServer("HitClaim", {

targetId = character:GetAttribute("EntityId"),

attackId = attackData.id,

timestamp = workspace:GetServerTimeNow()

})

-- Play hit effect immediately (client prediction)

playHitEffect(character)

end

end

-- Server: Validate hit claims

local function validateHit(player, data)

local attacker = player.Character

local target = getEntityById(data.targetId)

if not attacker or not target then return false end

-- Check distance (with tolerance for latency)

local distance = (attacker.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude

if distance > MAX_ATTACK_RANGE * 1.5 then return false end

-- Check attack is valid (cooldown, state)

if not canAttack(attacker, data.attackId) then return false end

-- Check target is damageable

if not canBeDamaged(target) then return false end

return true

end

```

Capsule Hitbox (For Character Shapes)

```lua

-- Multiple overlapping spheres for better character detection

local function capsuleOverlap(startPos, endPos, radius, ignoreList)

local params = OverlapParams.new()

params.FilterDescendantsInstances = ignoreList

params.FilterType = Enum.RaycastFilterType.Exclude

local hits = {}

local segments = 5

local direction = (endPos - startPos)

for i = 0, segments do

local t = i / segments

local pos = startPos + direction * t

local parts = workspace:GetPartBoundsInRadius(pos, radius, params)

for _, part in ipairs(parts) do

hits[part] = true

end

end

return hits

end

```

Damage Systems

Damage Calculation

```lua

local DamageSystem = {}

function DamageSystem.calculateDamage(baseDamage, attacker, target)

local damage = baseDamage

-- Attacker bonuses

local attackBonus = attacker:GetAttribute("AttackBonus") or 0

damage = damage * (1 + attackBonus / 100)

-- Critical hit

local critChance = attacker:GetAttribute("CritChance") or 5

local critMultiplier = attacker:GetAttribute("CritMultiplier") or 1.5

local isCrit = math.random(100) <= critChance

if isCrit then

damage = damage * critMultiplier

end

-- Target defense

local defense = target:GetAttribute("Defense") or 0

damage = damage * (100 / (100 + defense)) -- Diminishing returns formula

-- Elemental resistances

local element = attacker:GetAttribute("DamageElement")

if element then

local resistance = target:GetAttribute(element .. "Resistance") or 0

damage = damage * (1 - resistance / 100)

end

return math.floor(damage), isCrit

end

function DamageSystem.applyDamage(humanoid, damage, isCrit, source)

-- Check invincibility frames

if humanoid:GetAttribute("Invincible") then return end

humanoid:TakeDamage(damage)

-- Fire damage event for UI/effects

local character = humanoid.Parent

DamageEvent:Fire(character, damage, isCrit, source)

-- Track damage source for kill attribution

character:SetAttribute("LastDamageSource", source and source.Name or "Environment")

character:SetAttribute("LastDamageTime", os.clock())

end

```

Damage Over Time (DoT)

```lua

local DoTManager = {}

DoTManager.activeDoTs = {}

function DoTManager.applyDoT(target, dotData)

local id = HttpService:GenerateGUID()

local dot = {

id = id,

target = target,

damage = dotData.damage,

tickRate = dotData.tickRate or 1,

duration = dotData.duration,

element = dotData.element,

source = dotData.source,

startTime = os.clock(),

lastTick = 0

}

DoTManager.activeDoTs[id] = dot

return id

end

function DoTManager.update()

local currentTime = os.clock()

for id, dot in pairs(DoTManager.activeDoTs) do

-- Check expiration

if currentTime - dot.startTime >= dot.duration then

DoTManager.activeDoTs[id] = nil

continue

end

-- Apply tick damage

if currentTime - dot.lastTick >= dot.tickRate then

dot.lastTick = currentTime

local humanoid = dot.target:FindFirstChildOfClass("Humanoid")

if humanoid and humanoid.Health > 0 then

DamageSystem.applyDamage(humanoid, dot.damage, false, dot.source)

else

DoTManager.activeDoTs[id] = nil

end

end

end

end

```

Stun & Status Effects

Stun Handler Module

```lua

local StunHandler = {}

StunHandler.stunnedEntities = {}

-- Stun priorities (higher overrides lower)

local StunPriority = {

Stagger = 1, -- Brief interruption

Stun = 2, -- Standard stun

Knockdown = 3, -- Longer, on ground

Ragdoll = 4, -- Full physics ragdoll

Frozen = 5 -- Highest priority

}

function StunHandler.applyStun(character, stunType, duration)

local current = StunHandler.stunnedEntities[character]

local newPriority = StunPriority[stunType]

-- Only apply if higher or equal priority

if current and StunPriority[current.type] > newPriority then

return false

end

-- Apply stun

local humanoid = character:FindFirstChildOfClass("Humanoid")

if not humanoid then return false end

-- Disable movement

humanoid.WalkSpeed = 0

humanoid.JumpPower = 0

humanoid.AutoRotate = false

StunHandler.stunnedEntities[character] = {

type = stunType,

endTime = os.clock() + duration,

originalWalkSpeed = humanoid:GetAttribute("BaseWalkSpeed") or 16,

originalJumpPower = humanoid:GetAttribute("BaseJumpPower") or 50

}

-- Schedule recovery

task.delay(duration, function()

StunHandler.removeStun(character, stunType)

end)

return true

end

function StunHandler.removeStun(character, expectedType)

local stun = StunHandler.stunnedEntities[character]

if not stun or (expectedType and stun.type ~= expectedType) then return end

local humanoid = character:FindFirstChildOfClass("Humanoid")

if humanoid then

humanoid.WalkSpeed = stun.originalWalkSpeed

humanoid.JumpPower = stun.originalJumpPower

humanoid.AutoRotate = true

end

StunHandler.stunnedEntities[character] = nil

end

function StunHandler.isStunned(character)

return StunHandler.stunnedEntities[character] ~= nil

end

```

Knockback System

```lua

function applyKnockback(character, direction, force, duration)

local hrp = character:FindFirstChild("HumanoidRootPart")

if not hrp then return end

-- Normalize direction and apply force

local knockbackVelocity = direction.Unit * force

-- Use LinearVelocity for consistent knockback

local linearVel = Instance.new("LinearVelocity")

linearVel.Attachment0 = hrp:FindFirstChild("RootAttachment") or Instance.new("Attachment", hrp)

linearVel.VectorVelocity = knockbackVelocity

linearVel.MaxForce = math.huge

linearVel.RelativeTo = Enum.ActuatorRelativeTo.World

linearVel.Parent = hrp

task.delay(duration or 0.2, function()

linearVel:Destroy()

end)

end

```

Melee Combat

Combo System State Machine

```lua

local ComboSystem = {}

local ComboData = {

M1 = {

{name = "Jab", damage = 10, duration = 0.3, canCancel = {0.2, 0.3}},

{name = "Cross", damage = 12, duration = 0.35, canCancel = {0.25, 0.35}},

{name = "Hook", damage = 15, duration = 0.4, canCancel = {0.3, 0.4}},

{name = "Uppercut", damage = 20, duration = 0.5, canCancel = nil, finisher = true}

}

}

function ComboSystem.new(character)

return {

character = character,

currentCombo = nil,

comboIndex = 0,

lastAttackTime = 0,

inputBuffer = nil,

comboResetTime = 1.5

}

end

function ComboSystem.attack(self, attackType)

local currentTime = os.clock()

-- Reset combo if too much time passed

if currentTime - self.lastAttackTime > self.comboResetTime then

self.comboIndex = 0

self.currentCombo = nil

end

-- Get combo data

local combo = ComboData[attackType]

if not combo then return end

-- Advance combo

self.comboIndex = self.comboIndex + 1

if self.comboIndex > #combo then

self.comboIndex = 1

end

local attackData = combo[self.comboIndex]

self.currentCombo = attackType

self.lastAttackTime = currentTime

-- Execute attack

return self:executeAttack(attackData)

end

function ComboSystem.executeAttack(self, attackData)

-- Play animation

local animator = self.character:FindFirstChildOfClass("Humanoid"):FindFirstChildOfClass("Animator")

local track = animator:LoadAnimation(attackData.animation)

track:Play()

-- Create hitbox at appropriate frame

task.delay(attackData.hitboxDelay or 0.1, function()

meleeAttack(self.character, attackData.damage, attackData.knockback)

end)

return attackData

end

```

Ranged Combat

Hitscan Weapon

```lua

function fireHitscan(origin, direction, weaponData)

local params = RaycastParams.new()

params.FilterDescendantsInstances = {localPlayer.Character}

params.FilterType = Enum.RaycastFilterType.Exclude

-- Apply spread

local spread = weaponData.spread * (isADS and 0.3 or 1)

local spreadX = (math.random() - 0.5) * spread

local spreadY = (math.random() - 0.5) * spread

local spreadDir = (CFrame.lookAt(origin, origin + direction) * CFrame.Angles(spreadX, spreadY, 0)).LookVector

local result = workspace:Raycast(origin, spreadDir * weaponData.range, params)

if result then

-- Check if hit character

local character = result.Instance.Parent

local humanoid = character:FindFirstChildOfClass("Humanoid")

if humanoid then

-- Calculate damage falloff

local distance = result.Distance

local falloff = 1 - math.clamp((distance - weaponData.falloffStart) / (weaponData.range - weaponData.falloffStart), 0, 0.5)

local damage = weaponData.baseDamage * falloff

-- Headshot multiplier

if result.Instance.Name == "Head" then

damage = damage * weaponData.headshotMultiplier

end

CombatRemote:FireServer("HitscanHit", {

targetId = character:GetAttribute("EntityId"),

damage = damage,

hitPart = result.Instance.Name

})

end

-- Create impact effect

createImpactEffect(result.Position, result.Normal, result.Material)

end

return result

end

```

Projectile System

```lua

local ProjectileSystem = {}

ProjectileSystem.activeProjectiles = {}

function ProjectileSystem.spawn(data)

local projectile = {

id = HttpService:GenerateGUID(),

position = data.origin,

velocity = data.direction.Unit * data.speed,

gravity = data.gravity or Vector3.new(0, -workspace.Gravity, 0),

damage = data.damage,

owner = data.owner,

lifetime = data.lifetime or 10,

spawnTime = os.clock(),

radius = data.radius or 0.5

}

-- Create visual

projectile.visual = data.visualTemplate:Clone()

projectile.visual.CFrame = CFrame.lookAt(data.origin, data.origin + data.direction)

projectile.visual.Parent = workspace

ProjectileSystem.activeProjectiles[projectile.id] = projectile

return projectile

end

function ProjectileSystem.update(dt)

for id, proj in pairs(ProjectileSystem.activeProjectiles) do

-- Check lifetime

if os.clock() - proj.spawnTime > proj.lifetime then

ProjectileSystem.destroy(id)

continue

end

-- Physics update

proj.velocity = proj.velocity + proj.gravity * dt

local newPos = proj.position + proj.velocity * dt

-- Raycast for collision

local result = workspace:Raycast(proj.position, newPos - proj.position)

if result then

ProjectileSystem.onHit(proj, result)

ProjectileSystem.destroy(id)

continue

end

proj.position = newPos

proj.visual.CFrame = CFrame.lookAt(proj.position, proj.position + proj.velocity)

end

end

function ProjectileSystem.onHit(proj, rayResult)

local character = rayResult.Instance.Parent

local humanoid = character:FindFirstChildOfClass("Humanoid")

if humanoid then

DamageSystem.applyDamage(humanoid, proj.damage, false, proj.owner)

end

-- Spawn explosion/impact

createExplosion(rayResult.Position, proj.explosionRadius)

end

```

Ability System

Cooldown Management

```lua

local CooldownManager = {}

CooldownManager.cooldowns = {}

function CooldownManager.startCooldown(entity, abilityId, duration)

local key = entity:GetAttribute("EntityId") .. "_" .. abilityId

CooldownManager.cooldowns[key] = os.clock() + duration

end

function CooldownManager.isOnCooldown(entity, abilityId)

local key = entity:GetAttribute("EntityId") .. "_" .. abilityId

local endTime = CooldownManager.cooldowns[key]

return endTime and os.clock() < endTime

end

function CooldownManager.getRemainingCooldown(entity, abilityId)

local key = entity:GetAttribute("EntityId") .. "_" .. abilityId

local endTime = CooldownManager.cooldowns[key]

if not endTime then return 0 end

return math.max(0, endTime - os.clock())

end

```

Ability Base Class

```lua

local Ability = {}

Ability.__index = Ability

function Ability.new(data)

return setmetatable({

id = data.id,

name = data.name,

cooldown = data.cooldown,

manaCost = data.manaCost or 0,

castTime = data.castTime or 0,

onCast = data.onCast,

onChannel = data.onChannel,

onRelease = data.onRelease

}, Ability)

end

function Ability:canCast(caster)

-- Check cooldown

if CooldownManager.isOnCooldown(caster, self.id) then

return false, "On cooldown"

end

-- Check mana

local mana = caster:GetAttribute("Mana") or 0

if mana < self.manaCost then

return false, "Not enough mana"

end

-- Check stun

if StunHandler.isStunned(caster) then

return false, "Stunned"

end

return true

end

function Ability:cast(caster, target)

local canCast, reason = self:canCast(caster)

if not canCast then return false, reason end

-- Consume mana

local currentMana = caster:GetAttribute("Mana")

caster:SetAttribute("Mana", currentMana - self.manaCost)

-- Start cooldown

CooldownManager.startCooldown(caster, self.id, self.cooldown)

-- Execute ability

if self.castTime > 0 then

-- Channeled ability

self:startChannel(caster, target)

else

-- Instant ability

self.onCast(caster, target)

end

return true

end

```

More from this repository10

🎯
audio-system🎯Skill

Manages audio systems with advanced sound pooling, priority management, and performance optimization for immersive game sound experiences.

🎯
optimization🎯Skill

optimization skill from taozhuo/game-dev-skills

🎯
game-systems🎯Skill

Implements robust game systems like inventory, shops, trading, and progression mechanics for Roblox RPGs with secure, stackable item management.

🎯
animation-system🎯Skill

animation-system skill from taozhuo/game-dev-skills

🎯
vfx-effects🎯Skill

Generates dynamic visual effects like particle systems, camera shakes, and impact animations for enhancing game experiences in Roblox.

🎯
gemini-image-prompting🎯Skill

Generates conceptual game art images using Gemini AI, providing precise style-specific prompts for character, environment, and UI design across various game aesthetics.

🎯
security-anticheat🎯Skill

security-anticheat skill from taozhuo/game-dev-skills

🎯
roblox-api-patterns🎯Skill

Provides expert guidance on navigating Roblox-specific API behaviors, performance optimizations, and common coding pitfalls in Lua scripting.

🎯
data-persistence🎯Skill

Manages reliable Roblox data persistence by implementing robust data loading, saving, caching, and error handling for player progress and game state.

🎯
procedural-generation🎯Skill

Generates procedurally randomized content like terrain, dungeons, and cities using advanced noise algorithms and algorithmic techniques.