§ How to · with Picoo

How to make a Roblox sword fighting game (with AI)

By Sametcan Tasgiran, Founder & Developer·Published ·Updated

A Roblox sword-fighting game lives or dies on game-feel: a snappy M1 combo, a heavy attack, a block, a parry window that rewards timing, and a dash with invincibility frames. Ask Picoo for a sword fighting game with combos, block and parry, or simply for a combat system, and it applies its combat_system recipe, which builds all of these with every hit decided on the server.

5 files · 1,181 lines · one recipe call. HitboxModule and CombatServer in a CombatSystem folder, CombatClient for input and visuals, two RemoteEvents. Every player spawns holding a glowing StarterSword.

4-hit M1 combo

Click to swing. The server keeps its own combo counter, which resets after 0.8 s without a hit, and the client only sends the action name. Each hit deals 8 damage; the 4th is a finisher that deals 1.75x damage and knocks the target back.

Heavy attack (stamina-gated)

E triggers a heavy: 22 damage plus knockback, a 0.9 s cooldown and 25 of your 100 stamina, so it can't be spammed. Stamina refills at 15 per second.

Block: 75% damage reduction

Hold F to block. A blocked hit deals 25% of its damage with no knockback or hitstun, and a blue highlight shows your guard is up.

Parry window → attacker stun

Tap Q for a 0.35 s parry window. An M1 or heavy that lands inside it deals zero damage and stuns the attacker for 1.1 s, during which the server ignores their attacks, block, parry and dash.

Dash with i-frames

Shift dashes with a client LinearVelocity burst; the server grants 0.35 s of invincibility, a 1.2 s cooldown and a 20-stamina cost, so it reads as a real dodge, not a teleport.

Server hitbox (never .Touched)

Hit detection uses workspace:GetPartBoundsInBox on a box 9 studs deep in front of the attacker, with OverlapParams filtering out their own character. The R spin attack uses GetPartBoundsInRadius around them.

Files Picoo ships for this prompt

5 files · 1181 lines · one recipe call

ServerScriptService/CombatSystem/HitboxModule.lua

GetPartBoundsInBox hitbox that filters out the attacker and returns each enemy Humanoid once

60 lines

ServerScriptService/CombatSystem/CombatServer.lua

Owns all state and damage: combo, stamina, block, parry, stun, i-frames, spin attack; equips a neon StarterSword on spawn

514 lines

StarterPlayer/StarterPlayerScripts/CombatClient.lua

Binds M1, right-click stance, E/F/Q/R and Shift, sends intent, plays the arm swings and client dash, draws the stamina and combo HUD

607 lines

ReplicatedStorage/CombatRemotes/CombatAction

RemoteEvent the client fires with an action name and its combo index

instance

ReplicatedStorage/CombatRemotes/CombatState

RemoteEvent the server fires about 10 times a second with stamina and combo for the HUD

instance

Sample output: ServerScriptService/CombatSystem/HitboxModule.lua

--!strict
-- Generated by Picoo recipe: combat_system (HitboxModule)
-- Detects enemy Humanoids inside a box in FRONT of the attacker's
-- HumanoidRootPart using workspace:GetPartBoundsInBox. This is the modern,
-- reliable hitbox: it can't miss a fast swing the way .Touched does, and it
-- doesn't depend on the deprecated RaycastHitbox module.

local Players = game:GetService("Players")

local Hitbox = {}

-- Query a box reach studs ahead of the attacker, size wide/tall/deep.
-- Returns an array of enemy Humanoids (attacker's own character excluded).
function Hitbox.query(character: Model, reach: number, width: number, height: number): { Humanoid }
	local results: { Humanoid } = {}
	if not character then return results end

	local root = character:FindFirstChild("HumanoidRootPart")
	if not root or not root:IsA("BasePart") then return results end

	-- Box centred one half-reach in front of the attacker (local -Z is forward
	-- on a HumanoidRootPart), sized to cover a short-range melee arc.
	local depth = math.max(reach, 1)
	local boxCFrame = root.CFrame * CFrame.new(0, 0, -depth / 2)
	local boxSize = Vector3.new(math.max(width, 1), math.max(height, 1), depth)

	-- Exclude the attacker's own parts so you can never hit yourself.
	local overlapParams = OverlapParams.new()
	overlapParams.FilterType = Enum.RaycastFilterType.Exclude
	overlapParams.FilterDescendantsInstances = { character }
	overlapParams.MaxParts = 50

	local parts = workspace:GetPartBoundsInBox(boxCFrame, boxSize, overlapParams)

	-- Dedupe Humanoids: many parts belong to one character.
	local seen: { [Humanoid]: boolean } = {}
	for _, part in ipairs(parts) do
		local model = part:FindFirstAncestorOfClass("Model")
		if model and model ~= character then
			local humanoid = model:FindFirstChildOfClass("Humanoid")
			if humanoid and humanoid.Health > 0 and not seen[humanoid] then
				seen[humanoid] = true
				table.insert(results, humanoid)
			end
		end
	end

	return results
end

-- Resolve the Player who owns a Humanoid (nil for NPCs). Nil-safe helper the
-- server uses for parry lookups.
function Hitbox.playerFromHumanoid(humanoid: Humanoid): Player?
	if not humanoid then return nil end
	local model = humanoid.Parent
	if not model then return nil end
	return Players:GetPlayerFromCharacter(model)
end

return Hitbox

Building a Roblox sword fighting game

Sword-fighting is one of the highest-retention genres on Roblox — Blade Ball and countless anime combat games all run on the same core loop: a light-attack combo, a heavy, a block, a parry, and a dash. What separates a game that feels good from one that feels broken lives entirely in the details: does the combo reset at the right time, does the parry window reward timing, does the dash actually dodge, and — critically — is any of it exploitable.

The single most common way sword games get destroyed is client-trusted damage. If the client sends "I hit for 30", an exploiter sends "I hit for 9999" and one-shots the server. Picoo's combat_system recipe is built server-first: the client fires only an intent (the action name and the current combo index), and the server owns every damage calculation, cooldown, stamina check, and hitbox query. There is no code path where a client number becomes a damage number.

Hit detection uses workspace:GetPartBoundsInBox — a box query projected in front of the attacker's HumanoidRootPart, filtered to exclude the attacker's own character via OverlapParams. This is deliberately not .Touched, which misses fast swings and misfires under load, and not the deprecated RaycastHitbox module. The box query is deterministic: every swing checks exactly the volume in front of the attacker.

The skill layer is block and parry. Block (hold F) cuts incoming damage by 75% and cancels the knockback and hitstun. Parry (tap Q) opens a 0.35 s window; an M1 or heavy that lands inside it deals zero damage and stuns the attacker for 1.1 s — the timing-reward loop that every competitive sword game is built around. Dash (Shift) applies a client-side LinearVelocity burst for responsiveness while the server grants 0.35 s of invincibility and a 1.2 s cooldown, so it reads as a real dodge instead of a teleport. R adds a spin attack that hits everyone within 11 studs.

Every player also spawns holding a StarterSword, a Neon blade with a PointLight glow that follows the arm swings, and a HUD at the bottom of the screen shows stamina, the current combo and the controls. For an element look, ask for a fire (or ice, void, lightning) sword in a follow-up prompt: that adds the sword_tool recipe's glowing Tool with particles and a swing trail. Both are deterministic recipes, so the same request gives the same working result every time instead of the run-to-run variance of hand-prompted combat code.

See more on the Luau generator, the AI Luau script writer, the 3D model generator and the game builder, read how Picoo compares with ChatGPT, or browse the full blog.

Frequently asked

How is the combo protected from exploits?+

The client only sends an action name and its own combo index, never a damage number. The server keeps the real combo counter, checks the cooldown (0.28 s between M1s) and stamina, runs the hitbox itself and applies the damage. An exploiter can spam the RemoteEvent, but requests inside the cooldown or without stamina are ignored, and only the server decides who is in range.

Why not use .Touched for hit detection?+

.Touched misses fast weapon swings and fires unreliably under server load. combat_system uses workspace:GetPartBoundsInBox with an OverlapParams filter: a box 9 studs deep, 7 wide and 7 tall in front of the attacker's HumanoidRootPart, queried on every swing. It also avoids the deprecated RaycastHitbox module.

Does the parry actually stun the attacker?+

Yes. When an M1 or heavy lands inside the target's 0.35 s parry window, the server deals zero damage, plays a gold spark and a ping on the target, and stuns the attacker for 1.1 s: their attack, block, parry and dash requests are ignored until it wears off, and an M1 combo resets.

Can I retexture the sword to glow or flame?+

The StarterSword that combat_system gives every player already has a Neon blade with a cyan PointLight glow. For an element look, ask for a fire sword or katana in a follow-up prompt (ice, void, lightning, shadow and holy work too). That applies the sword_tool recipe, which adds a separate Tool to StarterPack with a Neon blade in the element's colour, a PointLight aura, element particles and a swing trail.

What should I type to get the full combat system?+

Name the mechanics: 'make a sword fighting game with combos, block and parry', 'build me a combat system' or 'melee combat with a parry window' all route to combat_system. A request for just a sword gets the sword_tool weapon on its own, which you can add next to the combat system.

Related Picoo prompts