Skip to content

Sweeps

Server-authoritative attack volumes for big rigs, authored as root-local capsule tracks, rebuilt from the live root every tick and sub-stepped so a fast swing cannot skip a target.

Realm
Server
Require
ServerScriptService.ChloeKernelServer.Sweeps
Since
v0.7.0
Source
Sweeps.luau

Sweeps gives big rigs hitboxes that do not lag. A dragon’s tail swipe or a giant’s club covers too much ground per tick for a single-position overlap check, and physics parts are the wrong source: during fast animation a monster’s parts trail its true server pose, exactly when the hit matters. Sweeps never reads a physics part. An attack’s volume is authored as root-local capsule tracks (pure functions of swing phase), rebuilt in world space from the rig’s live root CFrame every tick, and tested with segment math against each candidate. No Touched, no engine overlap query. Projectiles covers travel-time shots and Hazards covers area bands; MeleeKit covers humanoid-scale arc-and-range combat.

Where Sweeps fits next to Rewind, Projectiles and MeleeKit

Section titled “Where Sweeps fits next to Rewind, Projectiles and MeleeKit”
System Answers Victim position Attacker volume
Rewind Did a player’s claimed ranged shot land, given latency Rewound to the claimed time A ray
Projectiles Did a server-simulated shot’s path cross a player Current A swept segment per tick
MeleeKit Did a timed melee swing land, and who wins block and parry Current A range and arc at the hit frame
Sweeps Did a big rig’s authored attack volume pass through a player Current Root-local capsule tracks, sub-stepped

Sweeps is for attacks that are large or fast enough that an arc or a ray cannot describe them, and that the server starts (a monster decides to swing). There is no client claim, so there is no timestamp to rewind; “now” is the right instant to test.

Each definition has a Duration in seconds. A swing’s phase is age / Duration, clamped to 0..1, where age is Clock() - StartedAt (os.clock by default). A capsule track is a pure function (phase) -> Capsule that never sees wall time. Feed it 0.3 and it returns the capsule 30 percent through the swing, whatever the tick rate. That makes a track testable on its own.

export type Capsule = {
From: Vector3, -- root-local
To: Vector3, -- root-local
Radius: number,
}

From and To are offsets in the rig’s own space (“3 studs forward, 2 up”), never world positions. Each tick resolves the swing’s root pose and converts the points:

local From = Root:PointToWorldSpace(Shape.From)
local To = Root:PointToWorldSpace(Shape.To)

Only phase progress carries between ticks. A giant that turns or is knocked back mid-swing carries its capsule with it, because the capsule is rebuilt from where the root is now.

Root accepts:

Root value Resolves to
BasePart .CFrame
Model :GetPivot()
() -> CFrame Called every tick

When the root resolves to nil (an Instance that is neither a BasePart nor a Model, or a function that returns nil), the swing is dropped that tick without an error. A root function or capsule track that throws stops only its own swing with a warning naming the attack; the other swings keep sweeping. Without that isolation the error would repeat every tick and keep the timer awake.

A destroyed root part still reports its last CFrame, so a swing on a despawned rig runs out its Duration in place. Call Stop() when the rig dies.

The exact test is the closest approach between two 3D segments. Sweeps.segmentDistanceSquared(a1, a2, b1, b2) returns the squared distance between the closest points of the two segments, each clamped to its own ends. Two capsules overlap exactly when their core segments are within the sum of their radii. Sweeps.capsuleHitsVictim builds the victim as a vertical segment HalfHeight above and below the root and checks:

Sweeps.segmentDistanceSquared(from, to, Bottom, Top) <= (radius + victimRadius) ^ 2

Both functions are pure. This is a different primitive from Rewind’s rayCapsule, which tests a ray against a static capsule; Sweeps tests a moving capsule against a static one and does not require Rewind.

The only engine query Sweeps makes is the optional line-of-sight ray below, and only for a victim the capsules already caught.

Testing only the current phase would let a fast arc pass a target between two ticks. Each tick instead samples the phase interval since the previous tick:

local SubPhase = Swing.LastPhase + (Phase - Swing.LastPhase) * (Substep / Swing.Substeps)

For each candidate, Substep runs from its start to Substeps (default 3), evaluating every track at each interpolated phase and stopping at the first overlap. A whole swing that completes within one tick still gets Substeps samples across its arc.

The spec’s snap case makes this concrete: a Duration = 0.1 swing whose 180-degree arc elapses in one 0.2 s clock jump, with Substeps = 8, catches a victim sitting 90 degrees into the arc, a point testing only phase 0 and phase 1 would never sample. Sub-stepping is still discrete sampling at a finer grain, so a large or very fast arc needs a larger Substeps.

Sub-step 0 of a tick is the same phase as the last sub-step of the previous tick, so only the first tick tests it:

local FirstSubstep = if Swing.LastPhase > 0 then 1 else 0

With RequireLineOfSight = true on a definition, a victim the capsules caught must also be visible from the rig: Sweeps casts one solid-world ray (RespectCanCollide = true) from the root’s position to the victim’s root. Colliding geometry between them blocks the hit. The ray excludes the victim’s character and the rig: the root part’s ancestor model (or the root part alone when it has none), or the root model itself. A function root excludes nothing of its own. One RaycastParams is reused for every check.

A blocked victim is not added to the hit set, so it stays eligible for the rest of the swing: if the sight line clears on a later tick while the capsule still overlaps, the hit lands then. A tail that sweeps over a wall does not hit the player crouched behind it; the same tail rounding the wall does.

Each swing has its own HitSet, created in start. A candidate already in it is skipped; a victim is added before Sweep.Hit publishes. A slow arc that keeps a victim inside for several ticks registers one hit. A second start of the same attack gets a new, empty set, so the next swing can hit the same victim again.

With a Spatial option (a Spatial character index), each tick queries candidates within Reach (default 64) of the root’s position instead of taking every player. Reach must cover the farthest any track’s capsule extends from the root, plus the victim radius: a tail whose tip sits 45 studs out with a 2.5-stud radius needs at least 49.5 against the default victim. Without an index, the default GetTargets returns every player with a character, rebuilt each tick.

Candidates must be player-shaped: the victim position is Character.HumanoidRootPart.Position. Spatial.characters builds exactly that, keyed by Player.

Capsules is a list, so one attack can carry several volumes at the same phase (a tail and a trailing wing). A victim that overlaps any track counts for that tick; the tracks share the swing’s Duration, Reach and HitSet.

start returns a new swing each call, and the active set is keyed by swing, so a giant’s club swing and a stomp can run at once, each with its own clock and hit set. Swing.Stop() removes a swing from the active set at once; it never publishes another hit, even with Duration left. A stagger that interrupts an attack is one Stop() call.

attach creates no timer. The first start starts a Scheduler job at kernel.Priority.Normal, TickRate times per second (default 30). The step, or the Stop(), that leaves no active swing cancels it.

A giant’s club: a windup that pulls the club back, then a wide forward arc.

src/Server/Game/Services/GiantService.luau
--!strict
-- GiantService: the giant's club swing as a Sweeps attack.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local Spatial = require(ReplicatedStorage.ChloeKernel.Spatial)
local Sweeps = require(ServerScriptService.ChloeKernelServer.Sweeps)
-- Root-local track: windup, then strike. Offsets from the giant's root, never world positions
local function greatClubTrack(phase: number): Sweeps.Capsule
if phase < 0.4 then
local Pull = phase / 0.4
return { From = Vector3.new(0, 3, 1), To = Vector3.new(0, 3, 1 - Pull * 2), Radius = 2 }
end
local Progress = (phase - 0.4) / 0.6
local Angle = math.rad(-70) + math.rad(140) * Progress
return {
From = Vector3.new(0, 3, 0),
To = Vector3.new(math.sin(Angle) * 9, 3, -math.cos(Angle) * 9),
Radius = 2.5,
}
end
local GiantService = {
Name = "GiantService",
}
function GiantService.init(self: any, kernel: any)
self.Kernel = kernel
end
function GiantService.start(self: any)
self.Characters = Spatial.characters(self.Kernel)
self.Attacks = Sweeps.attach(self.Kernel, { Spatial = self.Characters })
self.Attacks:define("GiantClub", {
Duration = 0.9,
Reach = 14, -- the 9-stud tip, the 2.5 club radius, the 2 victim radius
Capsules = { greatClubTrack },
RequireLineOfSight = true,
})
-- Damage stays game-side: Sweeps reports who was hit and where
self.HitConnection = self.Kernel.Bus:subscribe(
"Sweep.Hit",
function(_topic: string, swing: any, victim: Player, _position: Vector3)
local Character = victim.Character
local Humanoid = Character and Character:FindFirstChildOfClass("Humanoid")
if swing.Name == "GiantClub" and Humanoid and Humanoid.Health > 0 then
Humanoid:TakeDamage(35)
end
end
)
end
-- Called by the giant's behavior when it decides to swing; keep the handle to Stop() it
function GiantService.swingClub(self: any, giant: Model): any?
return self.Attacks:start("GiantClub", { Root = giant })
end
function GiantService.stop(self: any)
if self.HitConnection then
self.HitConnection:disconnect()
end
if self.Attacks then
self.Attacks:destroy()
end
if self.Characters then
self.Characters:destroy()
end
end
return GiantService

The track’s -math.cos(Angle) puts the arc in front of the giant: a CFrame looks down its negative Z axis.

A dragon’s tail and wing sweep together and stop the instant the dragon is staggered:

Attacks:define("TailAndWing", {
Duration = 0.6,
Reach = 30,
Capsules = {
function(phase: number): Sweeps.Capsule -- the tail: long and narrow
local Angle = math.rad(160) * phase
return {
From = Vector3.new(0, 1, 0),
To = Vector3.new(math.sin(Angle) * 22, 1, math.cos(Angle) * 22),
Radius = 2.5,
}
end,
function(phase: number): Sweeps.Capsule -- the wing: shorter and wider
local Angle = math.rad(100) * phase
return {
From = Vector3.new(0, 6, 0),
To = Vector3.new(math.sin(Angle) * 14, 6, math.cos(Angle) * 6),
Radius = 4,
}
end,
},
})
local Swing = Attacks:start("TailAndWing", { Root = Dragon })
-- in the stagger handler:
if Swing then
Swing.Stop()
end

The client’s part is cosmetic: play the windup and strike animation on cue (AnimKit). Whether the attack lands never depends on what plays locally.

Sweeps.attach(kernel, options?) options:

Option Default Description
TickRate 30 Steps per second while a swing is active, at kernel.Priority.Normal
Spatial nil A Spatial.characters index; candidates come from a Reach query around the root
GetTargets players with a Character Roster when Spatial is absent
Clock os.clock Swing age clock (spec seam)
Raycast workspace:Raycast (origin, direction, params) -> RaycastResult? for the line-of-sight check (spec seam)
SkipLoop false Never schedule the step; call :step() yourself
Member Description
sweeps:define(name, definition) Registers an attack. Asserts Duration > 0 and at least one capsule track; a second define of the same name errors
sweeps:start(name, options) -> Swing? Starts a swing; nil for an unknown name or after destroy()
sweeps:step() Advances every active swing one tick: rebuild, sub-step, test, publish, retire. Scheduled automatically unless SkipLoop
sweeps:destroy() Cancels the step and clears every active swing
swing.Stop() Ends the swing now; no further hits
Sweeps.segmentDistanceSquared(a1, a2, b1, b2) -> number Pure: squared closest distance between two segments
Sweeps.capsuleHitsVictim(from, to, radius, victimPosition, victimRadius, victimHalfHeight) -> boolean Pure: whether a world-space capsule overlaps a vertical victim capsule

Definition fields:

Field Default Description
Duration required, above 0 Swing length in seconds
Capsules required, non-empty Root-local tracks (phase) -> Capsule, evaluated at every sub-step phase
Reach 64 Broad-phase radius around the root for the Spatial query
VictimRadius 2 Victim capsule radius
VictimHalfHeight 3 Victim capsule half-height
RequireLineOfSight false A caught victim must have clear solid-world sight to the root

SwingOptions (start):

Field Default Description
Root required BasePart, Model or () -> CFrame, read every tick
OnHit nil (victim, position) through task.spawn for each new victim, alongside Sweep.Hit
Substeps 3 Phase samples between the last tick’s phase and this tick’s
Topic Payload When
Sweep.Hit swing, victim, position A victim was hit for the first time this swing. swing is the swing table (Name, Definition, Root, Rig, OnHit, Substeps, StartedAt, LastPhase, HitSet, Stop); position is the victim’s root position at the hit

Damage stays game-side, as with MeleeKit and Projectiles: apply damage, knockback or effects from Sweep.Hit or OnHit. Sweeps never calls Humanoid:TakeDamage.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.