Seven abilities, one contract.
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. In Sword Masters each player brings one ability into the round and fires it with E or the button on screen. There are seven: Dash, a stronger God’s Dash for admins, Superjump, Stomp, Burger, Fireball and Laser beam. Some take effect instantly, one charges while you hold the key, and two last several seconds and need the player to aim in the middle. Not all of the system is mine. The base, meaning the client’s input layer, its cooldowns and the shop screens, came from another developer. I turned the server’s manager into a library that loads one module per ability, and I wrote the abilities. This article describes the contract every ability follows, the remotes that connect client and server, the three kinds of flow that come out of it, the per-player state the server keeps, and what the design leaves unprotected. Dash, Fireball and Laser beam have their own articles.
1. The problem
The seven abilities don’t look alike. Dash, Stomp and Burger are instant: you press and something happens. Superjump charges while you hold the key and jumps when you let go. Fireball and Laser beam last several seconds, play a sequence of animations, and in the middle need something only the client knows, which is where the player is aiming and when they click. All of them have to do their damage, movement and effects on the server, so other players see them and the client can’t decide how much damage it deals.
What I wanted was for adding an ability to mean writing one file, without touching the manager or the client.
2. The catalog
| Ability | Rarity | Price | Cooldown | Kind |
|---|---|---|---|---|
| God’s Dash | Admin | — | 0 s | instant |
| Dash | Starter | free | 5 s | instant |
| Superjump | Common | 4,500 | 13 s | hold and release |
| Stomp | Uncommon | 10,000 | 12 s | instant |
| Burger | Rare | 22,500 | 35 s | instant |
| Fireball | Epic | 35,000 | 22 s | long, aimed |
| Laser beam | Legendary | 67,500 | 44 s | long, aimed |
The data lives in a table per ability: image, cooldown, rarity, price, the IDs of its animations and its own numbers (Dash’s distance, Fireball’s range, Laser beam’s duration). Every new player starts with Dash equipped.
3. The contract
3.1 One module per ability
On the server, Server_Abilities_Handler/Use is a folder of modules. The manager requires every child except Lib (the shared helpers) and stores them by name, so the ability called "Fireball" is whatever the Fireball module returns. Each module exports four functions:
local main = {}
function main.Unlock() -- does releasing the key do anything?
return unlockable
end
function main.Load(plr, playerData, Loaded, bring, cback, kill, forceSL)
-- runs when the player presses the key
end
function main.Use(plr, playerData, Loaded, bring, cback, kill, forceSL)
-- runs when the client confirms the use
end
function main.Cleanup(plr, playerData)
return function() --[[ release what the ability created ]] end
end
return main
The signature is the same for every function in every module, even when one of them doesn’t use half the arguments. That’s what lets the manager dispatch by name without knowing anything about each ability, in the same spirit as the Command pattern [1]: the caller only knows the interface.
3.2 The remotes
Six channels connect the two sides:
| Remote | Type | Direction | What it means |
|---|---|---|---|
Load |
RemoteFunction | client → server | the key was pressed |
Unlock |
RemoteFunction | client → server | the key was released; returns whether that matters |
Use |
RemoteEvent | client → server | the client commits the use |
Loaded |
RemoteEvent | server → client | ready, you can commit |
Callback |
RemoteFunction | server → client | the server asks the client for something mid-ability |
Bring |
RemoteEvent | client → server | the client reports a step mid-ability |
There’s also a server-side BindableEvent, kill, which abilities fire when they kill someone so the kill shows up in the stats, and a Shiftlock event that Laser beam uses to force shift lock on the client.
4. Three flows
4.1 Instant
Dash, Stomp and Burger have a one-line Load:
function main.Load(plr, playerData, Loaded, ...)
Loaded:FireClient(plr, self);
return true;
end
The client presses, invokes Load, and the server answers by firing Loaded with the ability’s name. When it receives it, the client checks the cooldown, records the time, updates the slot on screen and fires Use. The effect lives in the server’s Use. It’s a round trip just to say “go”, but it gives the server the chance to say no before anything happens.
4.2 Hold and release
Superjump uses Load to charge. Once the player is on the ground, the server loop measures the time and stops when it reaches 4 s or when a flag says the key was released. When the player lets go, the client invokes Unlock, which for Superjump returns true, so the client fires Use, and the server’s Use sets the flag. The loop in Load sees it and jumps.
The charge is quantized. The time is rounded up to whole seconds and shifted, , and the jump’s velocity is
where is the direction the player is looking. A release before the first second gives , and each extra second adds one. The full charge has a quirk: the loop stops on the first frame where , which is always a fraction of a frame past 4, so and . That makes five possible jumps, from 105 studs/s forward and 60 up with a tap to 245 and 140 with the full charge.
4.3 Long
Fireball and Laser beam do everything inside Load. The function runs from start to finish as a script: play the takeout animation, wait for it to reach a certain point, ask the client for input through Callback:InvokeClient, run the effect, clean up and return. For these two, Unlock returns false, so letting go of the key does nothing, and the client fires Use only when Load returns, after the ability is over.
That has a nice consequence: the client records the time of use when it fires Use, so the cooldown of the long abilities starts when they end and not when they start. Laser beam’s 44 seconds are counted from the moment the beam goes out, not from the moment you press E.
Writing a long ability as a single function has a clear advantage. State lives in local variables (the projectile, the connections, the animation tracks), and the order of the phases is the order of the lines. The disadvantage shows up in section 6.
5. State on the server
The manager keeps a table per player:
playerData[plr.Name] = {
AbilityAnims = {}, -- [ability] = { AnimationTrack, ... }
AbilityData = {}, -- [ability] = scratch space for that ability
AbCooldown = false,
}
Every time the character spawns, the server loads the animations of the equipped ability into the Animator, with Action priority, in the order the data lists them. Each module then uses them by index (anims[1] is the takeout, anims[2] the idle, and so on). If an ability doesn’t define animations, it falls back to Dash’s with a warning instead of breaking. When the player leaves, Lib:MemoryBan walks the table recursively, disconnecting connections and destroying instances, so nothing is left hanging off a player who’s gone.
Abilities share state with the rest of the game through attributes on the character. Busy marks that a long ability is in progress: Fireball and Laser beam check it in Load and refuse to start if it’s set, so they can’t stack. MoreDamage is a damage multiplier that the strength power-up raises, and every ability that deals damage multiplies by it. MoreSpeed is a speed factor that Superjump and Laser beam lower while they run.
On the client, the abilities only work while the round is running and the player is in it. Two shared flags, IsRoundRunning and IsInRound, turn on 0.667 s after RoundStarted and turn off on RoundEnded or on death. The cooldown check is
with the ability’s cooldown, measured with os.clock() and a tenth of a second of margin.
6. Limitations
The server trusts the client too much. Cooldowns and the round flags are only checked on the client, and the server’s Use and Load don’t check either, so a modified client can fire Use as often as it wants. Fireball takes the projectile’s path from the client without validating it (its article goes into detail).
InvokeClient was the wrong tool for long abilities. If the client never answers, because it disconnects or because someone modified it, the server thread stays waiting forever, and if the client’s callback throws, the error comes back up to the server. Roblox’s own documentation warns about this [2]. A pair of RemoteEvents with a timeout does the same thing without handing the server’s control to the client.
There’s duplication left over from the inherited code. The abilities’ data lives in two modules with the same values, Classes/Abilities and Client_Modules/ReplicatedSettings, and the server’s code reads the second one. The system supports one slot, because the key configuration only has Slot1 = E. And there’s an eighth module, Decompile, that loads but whose Use is empty.
7. What I took away
The contract did what I wanted from it: adding Stomp or Burger was writing one file with four functions, and the manager didn’t change. Writing the long abilities as scripts inside Load was the right choice for reading them and the wrong one for safety, because a single function that waits on the client for seconds gives the client too much power over the server. If I did it again I’d keep the module per ability and the shared signature, but I’d move every check (cooldown, round, Busy) into the manager before calling the module, and I’d make each ability a small state machine driven by events instead of a function that waits.
References
- E. Gamma, R. Helm, R. Johnson, J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994.
- Roblox Creator Documentation. “Remote events and callbacks”.
This text by Mastedore. All rights reserved.