TeamKit
Holds team assignment for the server, with capacity-weighted auto-balance, map-authored team spawns and friendly-fire vetoes on both combat gates.
- Realm
- Server
- Require
ServerScriptService.ChloeKernelServer .Kits .TeamKit - Since
- v0.4.0
- Source
- Kits/TeamKit.luau
TeamKit is the one place the server records who is on which team. Friendly fire is enforced as vetoes on the hook points
where damage happens: WeaponKit’s Weapon.CanDamage and MeleeKit’s
Melee.CanHit. Any combat path that fires those gates follows team policy without knowing teams exist. NPC handles
resolve through their NPCKit Archetype.Faction, so an NPC whose faction matches a team name counts
as that team’s member. PartyKit handles social groups and never touches the combat gates.
Mental model
Section titled “Mental model”The kit keeps one map, ByPlayer: subject -> teamName, and derives the rest from it:
- Auto-balance. Each new session joins the team with the lowest load: member count divided by
Capacitywhen the team sets one, the plain count otherwise. Capacity weighting lets a 2-slot team fill in proportion to 8-slot teams instead of taking every other join. - Friendly fire. With
FriendlyFire = false(the default), one veto function is registered on both combat gates at priority 40, ahead of game handlers at the default priority 100. It returnsfalseonly when attacker and victim resolve to the same non-nil team, andnilotherwise. An unaffiliated subject is never vetoed. - Team spawns. Parts tagged
SpawnTag(default"TeamSpawn") with aTeamattribute naming the owning team. Only parts in the Workspace count; a tagged spawn in a stored map is ignored. See team spawns. - Roblox teams.
SyncRoblox = truecreates realTeaminstances withAutoAssignableoff and one distinctTeamColoreach, for the player list and for engine spawning.
teamOf accepts several subject shapes: a Player (or anything assigned directly), a session (through .Player), a
character Model (through GetPlayerFromCharacter) or an NPC handle (through Archetype.Faction). nil means
unaffiliated.
TeamKit is an attach kit. Attach it from the owning service’s init with the team table from Game/Config:
--!strict-- Teams: the arena's sides.
return { Red = { Color = Color3.fromRGB(200, 60, 60), Capacity = 8 }, Blue = { Color = Color3.fromRGB(60, 60, 200), Capacity = 8 },}--!strict-- ArenaTeamService: sides, spawns and friendly fire.
local ServerScriptService = game:GetService("ServerScriptService")
local TeamKit = require(ServerScriptService.ChloeKernelServer.Kits.TeamKit)local Teams = require(script.Parent.Parent.Config.Teams)
local ArenaTeamService = { Name = "ArenaTeamService",}
function ArenaTeamService.init(self: any, kernel: any) self.Kernel = kernel -- Friendly-fire vetoes register on Weapon.CanDamage and Melee.CanHit here self.Teams = TeamKit.attach(kernel, { Teams = Teams, SyncRoblox = true, -- real Team instances: player list colors and engine spawning })end
function ArenaTeamService.start(self: any) self.Connection = self.Kernel.Bus:subscribe("Team.Assigned", function(_topic, player, teamName, previous) self.Kernel.Bus:publish("Arena.TeamChanged", player, teamName, previous) end)end
function ArenaTeamService.stop(self: any) if self.Connection then self.Connection:disconnect() end if self.Teams then self.Teams:destroy() endend
return ArenaTeamServiceMap setup: in the Workspace, tag each spawn part TeamSpawn and set its string attribute Team to "Red" or "Blue".
Use SpawnLocation parts to let the engine place characters (see team spawns).
Attach options
Section titled “Attach options”| Option | Default | Description |
|---|---|---|
Teams |
required | { [teamName]: TeamConfig }, non-empty. Asserted at attach. |
AutoAssign |
true |
Assign each new session to the lowest-load team. |
FriendlyFire |
false |
false vetoes teammate damage on both gates. true registers no vetoes. |
SpawnTag |
"TeamSpawn" |
CollectionService tag for spawn parts. |
UseSpawns |
true |
Place characters at owned spawns. Only false turns it off. |
SyncRoblox |
false |
Mirror teams into Teams service instances with distinct TeamColors. assign and unassign then set player.Team and player.Neutral. |
Seed |
random | Seeds the spawn pick, for deterministic specs. |
Team config
Section titled “Team config”| Field | Description |
|---|---|
Color |
The team’s color. With SyncRoblox, converted with BrickColor.new(Color) for the TeamColor. |
Capacity |
Load divisor for auto-balance. A team without one balances by plain member count. |
Friendly fire on both gates
Section titled “Friendly fire on both gates”The veto is one function registered twice:
local function veto(context) local AttackerTeam = Teams:teamOf(context.Attacker) local VictimTeam = Teams:teamOf(context.Victim) if AttackerTeam ~= nil and AttackerTeam == VictimTeam then return false -- same team: block the damage end return nil -- different teams or unaffiliated: no opinionendBoth gates are fail-open hook points: handlers that return nil let damage through and any false
blocks it. Safe zones, duel pacts and round rules stack as more handlers on the same chain.
| Gate | Fired by | Context |
|---|---|---|
Weapon.CanDamage |
WeaponKit, before a validated shot deals damage | { Session, Attacker, Victim, WeaponId, Weapon } |
Melee.CanHit |
MeleeKit, per victim inside a swing’s hit frame | { Attacker, Victim, Attack } |
teamOf resolves every subject shape the combat kits pass, so the same veto covers player against player, player
against NPC and NPC against NPC.
Faction and NPC parity
Section titled “Faction and NPC parity”teamOf(npcHandle) returns Archetype.Faction. Give NPCs the same faction string as a team name and every check
agrees on who is friendly:
Npcs:define("RaiderGrunt", { Model = ServerStorage.Npcs.Raider, Faction = "Red", -- reads as team "Red" in every TeamKit check Targetable = true,})- A Red player shooting a
Faction = "Red"NPC through WeaponKit is vetoed. - A Blue player shooting the same NPC passes.
- Inside NPCKit,
Factionalso stops same-faction NPCs from targeting each other. Keeping NPCs from targeting players on their team is roster policy (GetTargetson the NPCKit); the TeamKit veto still blocks the damage if one gets through a gate.
The match is plain string equality with no registration step. A faction that matches no team name never collides with a player’s team.
Assignment and balancing
Section titled “Assignment and balancing”assign(player, teamName) returns false for an unknown team and true without an event when the player is already on
that team. Otherwise it updates the map, sets player.Team and player.Neutral = false when syncing, points the
player’s next spawn at an owned SpawnLocation (see below) and publishes Team.Assigned(player, teamName, previous).
previous is nil on a first assignment, which is how a scoreboard tells a join from a switch.
unassign(player) leaves the player unaffiliated, so they are never vetoed. It returns false when the player had no
team. Otherwise it clears the entry, sets player.Team = nil and player.Neutral = true when syncing, clears the
RespawnLocation the kit set and publishes Team.Unassigned(player, previous).
Auto-assignment runs once per session start when AutoAssign is on. A leave only removes the leaver’s entry and pending
spawn move; the kit never rebalances. Mid-round rebalancing is a game rule, so wire it where your rules want it, for
example on Kernel.SessionEnd between rounds using players() and emptiest(). Switching a player mid-session is
another assign; the next spawn uses the new team’s spawns.
Team spawns
Section titled “Team spawns”On every CharacterAdded for an assigned player (unless UseSpawns = false), the kit collects the Workspace parts
tagged SpawnTag whose Team attribute is the player’s team. If there are none, or the player has no team, the
character stays where the engine put it.
When some of those parts are SpawnLocations the engine accepts for the player (Neutral, or a TeamColor equal to
the team’s), the kit sets one at random as player.RespawnLocation and the engine places the character itself. The kit
sets it on assign and picks a new one on each spawn for the next life. It only clears a RespawnLocation it set.
Any other tagged part is a pivot target. CharacterAdded fires before the character is parented, and the engine moves
the character to its spawn after parenting, so the kit waits until the character is in the Workspace, defers once more,
checks it is still the player’s current character, publishes AntiExploit.Forgive so the
movement monitor pardons the move, and pivots to spawn.CFrame + Vector3.new(0, 4, 0).
Before 0.9.0 the pivot ran before the engine’s own spawn move, which could undo it.
Distinct TeamColors
Section titled “Distinct TeamColors”The engine matches teams, spawns and the player list by TeamColor, so with SyncRoblox every team gets a different
one. Teams with a Color use BrickColor.new(Color). If two colors round to the same BrickColor, attach errors and
names both teams. Teams without a Color take the first unused of Red, Blue, Green, Yellow and Black, then the
BrickColor palette, never White (a neutral player’s TeamColor reads white). All colors are resolved before any Team
instance is created, so an error leaves nothing behind.
API reference
Section titled “API reference”| Member | Description |
|---|---|
TeamKit.attach(kernel, options) -> kit |
Resolves TeamColors and creates Roblox teams when syncing, registers the friendly-fire vetoes, and binds auto-assign and spawning to sessions. |
kit:assign(player, teamName) -> boolean |
Manual assignment. false for an unknown team; publishes Team.Assigned on a change. |
kit:unassign(player) -> boolean |
Leaves the player unaffiliated. false when not on a team; publishes Team.Unassigned. |
kit:teamOf(subject) -> string? |
Resolves a Player, session, character Model or NPC handle. nil means unaffiliated. |
kit:sameTeam(a, b) -> boolean |
Both resolve to the same non-nil team. |
kit:players(teamName) -> { Player } |
Current directly assigned members. |
kit:emptiest() -> string |
The lowest-load team, weighted by capacity. |
kit:destroy() |
Disconnects the session wiring and pending spawn moves, removes the vetoes, destroys mirrored Team instances and clears assignments. |
Hooks and Bus topics
Section titled “Hooks and Bus topics”TeamKit registers handlers on two fail-open gates and defines no hook points of its own:
| Hook point | Kit’s role | Priority |
|---|---|---|
Weapon.CanDamage |
Veto: same non-nil team returns false, otherwise nil. |
40 |
Melee.CanHit |
The same veto. | 40 |
| Topic | Payload | When |
|---|---|---|
Team.Assigned |
player, teamName, previous? |
Every assignment change. previous is nil on the first. |
Team.Unassigned |
player, previous |
unassign removed a player from a team. Not published when a session ends. |
AntiExploit.Forgive |
player |
Published before each spawn pivot. |