An arc you bend with the mouse.
Warning
Sword Masters was abandoned in October 2024, and most of its animations, sounds and images belong to the group that owned it, so the game can’t run as it did and this system can’t be shown on video.
Abstract. Fireball is the Epic ability in Sword Masters. When you press E, a ball of fire grows in the character’s left hand. A green disc follows the mouse over the ground to mark the target. You click and hold to lock it, drag the mouse up or down to make the arc taller or flatter, and release to throw. The ball flies along the arc and explodes when it touches the map, dealing 80 damage in a sphere. It’s a long ability in the sense of the ability system: the server runs it from start to finish inside Load and, in the middle, asks the client for the trajectory through a RemoteFunction. The client computes the arc as a list of points about one stud apart, and the server moves the ball along that list with an AlignPosition, one point per frame. This article goes through the sequence, the shape of the reachable area, the arc’s formula and how it compares to a parabola, the flight, the explosion, and the trust the server places in the client.
1. The problem
A thrown projectile needs a trajectory the player can see before throwing and can shape. The obvious version is physics: give the ball a velocity and let gravity draw the parabola. That gives the player little control over where it lands, and predicting the landing point on the client to draw a guide means simulating the same physics on both sides. I preferred to define the trajectory as an explicit curve between the hand and the target. The player picks the target and the height, the guide drawn on screen is the curve itself, and the ball follows exactly what the player saw.
2. The sequence
The server does this, in order:
- Checks the character’s
Busyattribute and refuses to start if it’s set, then sets it. - Clones the ball from the module’s template, welds it to the left hand and grows it from 0.1 studs to its full size in one second, playing the invoke and idle animations.
- Connects
Bring. When the client reports that the player clicked, the server plays the aiming animation and freezes it at 4.81 s, holding the arm back withAdjustSpeed(0). - Calls
Callback:InvokeClient(plr, "Fireball", true)and waits for the list of points. - Plays the throwing animation, and when it reaches 0.35 s it destroys the weld and lets the ball go.
- Moves the ball through the points, explodes it on contact, clears
Busyand returns.
The client does its part while the server is waiting at step 4. It waits for the character to be on the ground, aims, waits for the click, fires Bring, turns the character towards the target and anchors it, draws the arc, lets the player bend it, and returns the points when the button is released or after 3 seconds, whichever comes first.
3. Aiming
Every frame, the client casts a ray from the camera through the mouse and places the disc where it hits, clamping each coordinate to within 75 studs (MaxRadius) of the character’s root:
The clamp is per axis, so the reachable area isn’t a sphere of radius 75 but a cube with sides of 150 studs centered on the player. Along the axes the reach is 75 studs; along the diagonals of the ground it goes up to , and at the cube’s corners to . A player who aims diagonally across the world’s axes throws further than one who aims straight along one of them. Clamping the distance, , would give a circle of the same radius in every direction.
The actual target, , sits 3 studs above the disc, so the ball arrives at the height of a character’s torso and not at the ground.
4. The arc
4.1 The formula
When the player clicks, the client counts , where is the ball’s position in the hand, and creates small spheres, alternating between visible and invisible to draw a dashed line. Sphere goes at
a straight line between the hand and the target with a vertical bump that’s zero at both ends and in the middle. The spacing between points along the line is stud.
4.2 Bending it
While the button is held, the height follows the vertical movement of the mouse since the click, in pixels (positive downwards):
It starts at 6 studs. Moving the mouse up 270 pixels takes it to the 36-stud ceiling, and moving it down 54 pixels flattens it to 0. Further down, the absolute value makes it rise again, so the function is a V and not a slider: dragging far down ends up making the arc tall again. It’s not what I’d design now.
4.3 It isn’t a parabola
The bump is a half sine, not the parabola gravity would draw. With the same apex, the parabola is , and the two curves compare like this:
with equality only at the ends and at the middle. The sine leaves the hand with a slope of against the parabola’s , about 21% less, and stays slightly below the parabola along the whole way. Its area is against . To the eye they’re almost the same, but the sine makes the ball’s vertical acceleration change along the path ( per unit of squared, zero at the ends) instead of being constant like gravity’s. It looks fine for a magical ball of fire. It would look wrong for a rock.
5. The flight
WaypointMove is an AlignPosition attached to the ball. The server sets its target to the first point and then advances one point per frame:
while true do
if not fireball then break; end;
pcall(function() fireball:SetNetworkOwner(plr) end)
position = math.clamp(position + 1, 1, #positions)
if position%2 then runSvc.Heartbeat:Wait(); end
ao.Position = positions[position];
if position == #positions then break; end;
end
The target, then, moves at about one stud per frame, 60 studs per second at 60 Hz, and the throw lasts seconds: a little over a second for a 75-stud throw. The ball follows the target with the lag of the constraint. Handing the network ownership to the thrower makes their client simulate the ball, so they see it fly smoothly.
position%2 looks like an attempt to wait only on odd points, which would have doubled the speed. In Luau 0 is truthy, so the condition is always true and the loop waits on every point.
6. The explosion
While it flies, a Heartbeat connection checks which map parts the ball overlaps, using GetPartsInPart. When one of them belongs to the destructible geometry (inside a Destroyables folder or carrying that tag or attribute), the ball explodes where it is. If the path ends without touching anything, the server destroys the AlignPosition and the ball falls under gravity until it touches the map and explodes then.
The explosion creates a part with the effect’s particles, which Debris removes after 20 s, and a spherical part used as the damage volume. GetPartsInPart with a sphere uses the real shape, so the damage area is exactly a sphere. Its size comes from DamageAreaRadius, 33, but Size is a diameter, so the real radius is 16.5 studs. Each character inside takes once. In PvP that includes other players. Outside PvP it only includes NPCs. A tag with a random suffix (Burned_ plus a number from 0 to 3690) marks the characters that were already hit, so the same explosion doesn’t damage anyone twice. A tenth of a second after each hit, the server checks whether the target died and, if so, credits the kill.
7. Limitations
The biggest one is trust. The server takes the list of points the client returns and flies the ball through it without checking anything: not the number of points, not the spacing, not that the target is within 75 studs. A modified client can return a path that ends anywhere on the map. The fix is cheap, because the arc is determined by three things, , and . The client should send only and , and the server should clamp them and recompute the points. The same applies to everything the ability system article says about InvokeClient: if the client doesn’t answer, Load waits forever with the ball in the hand.
The explosion check yields inside a Heartbeat callback (it waits a frame before emitting the particles and a tenth of a second per hit), and each frame starts a new call to that callback. Until the first explosion finishes and destroys the ball, the following frames can detect the same contact and explode again. The tag keeps the damage from repeating, but the burst of particles does repeat. The tags are never removed either, and with 3,691 possible suffixes, two fireballs can share one.
Some names don’t say what they do: MaxRadius is the half-side of a cube, DamageAreaRadius is a diameter, and the 3 seconds of MaxChargeTime in the data aren’t read, because the client has the 3 hard-coded.
8. What I took away
Defining the trajectory as an explicit curve was the right decision for this ability: what you see is what you get, and the formula fits in one line. What I’d change is where the curve lives. If the server computes it from a target and a height, the client only has to propose those two numbers, the server can validate them in one line each, and InvokeClient disappears. The position%2 bug taught me to check truthiness with the language’s rules and not with the ones I have in my head from other languages.
This text by Mastedore. All rights reserved.