combat-system
π―Skillfrom taozhuo/game-dev-skills
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.
Installation
npx skills add https://github.com/taozhuo/game-dev-skills --skill combat-systemSkill Details
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
Manages audio systems with advanced sound pooling, priority management, and performance optimization for immersive game sound experiences.
optimization skill from taozhuo/game-dev-skills
Implements robust game systems like inventory, shops, trading, and progression mechanics for Roblox RPGs with secure, stackable item management.
animation-system skill from taozhuo/game-dev-skills
Generates dynamic visual effects like particle systems, camera shakes, and impact animations for enhancing game experiences in Roblox.
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 from taozhuo/game-dev-skills
Provides expert guidance on navigating Roblox-specific API behaviors, performance optimizations, and common coding pitfalls in Lua scripting.
Manages reliable Roblox data persistence by implementing robust data loading, saving, caching, and error handling for player progress and game state.
Generates procedurally randomized content like terrain, dungeons, and cities using advanced noise algorithms and algorithmic techniques.