chief/traits
The Traits extension for Chief: binds behavior to Roblox instances through CollectionService tags.
chief/traits
The Traits extension for Chief: binds behavior to Roblox instances through CollectionService tags.
A trait is a regular Chief module carrying an Info contract. When an instance gains the trait's tag — and satisfies the declared instance class, attributes, and children — the extension constructs a bound object and runs its lifecycle. When the tag is removed or the instance is destroyed, the trait unbinds and cleans up after itself.
Installation
pesde add chief/traits
Usage
Register trait modules through Traits.new — not through AddModules. A trait's Init/Start receive a per-instance bound object, not the module itself, so Chief's boot loop (which calls Init/Start on every added module) must never see them:
--!strict
local Chief = require('path/to/chief')
local Traits = require('path/to/traits')
Chief.new()
:AddModules(script.Modules:GetChildren())
:AddExtension(Traits.new(script.Traits:GetChildren()))
:Start()
A trait module declares its contract with Traits.info and implements per-instance lifecycle methods:
--!strict
local Traits = require('path/to/traits')
local Door = {}
Door.Info = Traits.info({
Tag = 'Door',
InstanceIs = 'BasePart',
Attributes = {
OpenAngle = Traits.attribute('number', 90), -- Has a default -> optional
Locked = Traits.attribute('boolean'), -- No default -> required
},
Children = {
Prompt = Traits.child('ProximityPrompt', { Wait = 5 }),
},
})
function Door.Init (self: Self)
-- Synchronous, runs once per bound instance. Yielding here errors.
self.Bin:Add(self.Prompt.Triggered:Connect(function ()
self:Toggle()
end))
end
function Door.Start (self: Self)
-- Spawned after Init. Safe to yield or loop here; the thread is cancelled automatically when the trait unbinds.
end
function Door.Toggle (self: Self)
if self.Attributes.Locked then
return
end
print(`{self.Instance.Name} toggled by {self.Attributes.OpenAngle} degrees`)
end
function Door.AttributeChanged (self: Self, name: string, value: any)
-- Optional: called when a declared attribute changes (self.Attributes is already updated).
end
function Door.Destroy (self: Self)
-- Optional: runs on unbind, before the Bin empties itself. Yielding here errors.
end
type Self = typeof(Door) & Traits.Bound & { Prompt: ProximityPrompt }
return Door
Lifecycle
- Watching begins at
PostStart— after Chief's entire boot barrier. Every module is initialized before the first trait binds, so trait bodies can safely require and use any Chief module (traits don't need dependency injection: a plainrequireworks). - Bind — when an instance has the tag and fulfills the contract: children are resolved (waiting up to
Waitseconds if given), attributes are validated (defaults written back to the instance, so they show in Studio), thenInitruns synchronously andStartis spawned. - Unbind — when the tag is removed or the instance is destroyed:
Destroyruns, then theBinempties — which also cancels a still-runningStartthread. A bind still waiting on a child is simply cancelled.
An instance that fails the contract is not bound, and a warning names the exact requirement that failed — a tagged instance silently doing nothing is much harder to debug.
One erroring instance never breaks the others: lifecycle failures are contained per instance, reported with a full traceback.
API
Traits.info(properties): Info
Creates the trait's contract; assign it to the module's Info field (Traits.new requires every registered module to carry one). Validated eagerly — a malformed contract errors at require time.
Tag: string— the CollectionService tag to watch.InstanceIs: string?— class (or superclass) the tagged instance must be. Defaults to'Instance'.Attributes: { [string]: Attribute }?— attribute requirements.Children: { [string]: Child }?— child requirements. Each key is both the child's expectedNameand the field the resolved child is injected under onself.
Traits.attribute(kind, default?): Attribute
Declares an attribute requirement. kind is a typeof() name ('string', 'number', 'boolean', 'Vector3', 'Color3', 'CFrame', and every other Roblox attribute type). With a default the attribute is optional and the default is written to instances that lack it; without one it's required.
Traits.child(className, options?): Child
Declares a required child. options.Wait (seconds) waits for the child to appear — useful under StreamingEnabled — instead of failing immediately.
Traits.new(moduleScripts): Extension
Creates the extension, wiring the given trait ModuleScripts. Add it to Chief with AddExtension. Trait modules are required and validated eagerly, so a malformed trait fails at composition instead of surfacing later as an inert tag.
Traits.get(instance, trait): Bound?
The bound object a trait has attached to an instance, or nil.
Traits.getAll(trait): { Bound }
Every bound object of a trait, in no particular order.
The bound object (self)
Lifecycle methods receive a per-instance object:
self.Instance— the tagged instance.self.Attributes— declared attribute values, kept in sync as they change.self.Bin— a cleanup container.Bin:Add(item)accepts a connection (disconnected), instance (destroyed), thread (cancelled), or function (called); everything is cleaned in reverse order automatically on unbind.- One field per declared child (e.g.
self.Prompt). - Everything on the trait module itself, via the metatable.
Type it by intersection: type Self = typeof(Door) & Traits.Bound & { Prompt: ProximityPrompt }.
Errors and warnings
BadArgument:— thrown whennew,info,attribute,child,get,getAll, orBin:Addreceive the wrong shape (including reserved child names likeInstance,Bin,Init, and modules passed tonewwithout anInfocontract).BadInstance:(warning) — a tagged instance failed the contract; names the failed requirement.BadAttribute:(warning) — a declared attribute changed to a value of the wrong type; the change is ignored.TraitError:— a trait'sInitorDestroyerrored (or yielded — both must be synchronous); reported with traceback, cleanup still runs.DuplicateTrait:(warning) — the same trait module was wired twice (e.g. added to two Chief instances).
License
MIT