Skip to main content

BehaviorBuilder

Fluent typed configuration builder for Vetra.

Instead of constructing raw behavior tables by hand, chain namespace methods and call :Build() to produce a validated, frozen VetraBehavior.

local Behavior = BehaviorBuilder.new()
    :Physics()
        :MaxDistance(500)
        :MinSpeed(5)
    :Done()
    :Bounce()
        :Max(3)
        :Restitution(0.7)
        :Filter(function(context, result, vel)
            return result.Instance:HasTag("Bouncy")
        end)
    :Done()
    :Drag()
        :Coefficient(0.003)
        :Model(Vetra.Enums.DragModel.G7)
    :Done()
    :Build()

Namespace overview:

Method Configures
:Physics() MaxDistance, MaxSpeed, MinSpeed, Gravity, Acceleration, RaycastParams, CastFunction, BulletMass
:Homing() Filter, PositionProvider, Strength, MaxDuration, AcquisitionRadius
:Pierce() Filter, Max, SpeedThreshold, SpeedRetention, NormalBias, PierceDepth, PierceForce, ThicknessLimit
:Bounce() Filter, Max, SpeedThreshold, Restitution, MaterialRestitution, NormalPerturbation, ResetPierceOnBounce
:HighFidelity() SegmentSize, FrameBudget, AdaptiveScale, MinSegmentSize, MaxBouncesPerFrame
:CornerTrap() TimeThreshold, PositionHistorySize, DisplacementThreshold, EMAAlpha, EMAThreshold, MinProgressPerBounce
:Cosmetic() Template, Container, Provider
:Debug() Visualize
:Drag() Coefficient, Model, SegmentInterval, CustomMachTable
:Wind() Response
:Magnus() SpinVector, Coefficient, SpinDecayRate
:GyroDrift() Rate, Axis
:Tumble() SpeedThreshold, DragMultiplier, LateralStrength, OnPierce, RecoverySpeed
:Fragmentation() OnPierce, Count, Deviation
:SpeedProfiles() Thresholds, :Supersonic() → profile, :Subsonic() → profile
:Trajectory() Provider
:LOD() Distance
:SixDOF() Enabled, LiftCoefficientSlope, PitchingMomentSlope, PitchDampingCoeff, RollDampingCoeff, AoADragFactor, ReferenceArea, ReferenceLength, AirDensity, MomentOfInertia, SpinMOI, MaxAngularSpeed, InitialOrientation, InitialAngularVelocity, CLAlphaMachTable, CmAlphaMachTable, CmqMachTable, ClpMachTable
:BatchTravel() Root-level boolean toggle, no sub-builder
:Hitscan() Root-level boolean toggle, no sub-builder
:Clone() Returns an independent copy of this builder
:Impose(other) Copies only the explicitly-set fields from other onto self
:Merge(a, b, ...) Clone + impose multiple modifiers, returns new builder
:When(cond, fn) Conditionally apply a block without breaking the chain
BehaviorBuilder.Inherit(frozen) Create a builder from a frozen VetraBehavior table
DragModel enum

Use Vetra.Enums.DragModel when passing a drag model to :Drag():Model(). A wrong integer is silently incorrect; an invalid enum key is a nil-index warning immediately at the call site:

:Drag():Model(Vetra.Enums.DragModel.G7):Done()

See Enums.DragModel for the full value table and descriptions.

6DOF quick-start

Enable full six-degrees-of-freedom aerodynamics to give bullets real pitch, yaw, and roll dynamics driven by lift, pitching moment, damping, and gyroscopic precession.

Minimum required fields when :SixDOF():Enabled(true):

Field Why required
BulletMass (via :Physics()) Converts aerodynamic force vectors into acceleration, F = ma
ReferenceArea Cross-sectional area in studs², scales all aero forces
ReferenceLength Caliber/diameter in studs, scales pitching moment and damping
MomentOfInertia Transverse MOI, governs pitch/yaw angular response

Minimal example:

local Behavior = BehaviorBuilder.new()
    :Physics()
        :BulletMass(0.01)
        :MinSpeed(10)
    :Done()
    :Drag()
        :Coefficient(0.003)
    :Done()
    :SixDOF()
        :Enabled(true)
        :ReferenceArea(0.008)
        :ReferenceLength(0.05)
        :MomentOfInertia(0.001)
        :LiftCoefficientSlope(2.0)
        :PitchingMomentSlope(-0.5)
        :PitchDampingCoeff(0.02)
    :Done()
    :Build()

All other 6DOF fields default to safe values (0 or false). Start here and add AoADragFactor, RollDampingCoeff, and spin fields as needed.

6DOF + gyroscopic precession

Gyroscopic precession, the bullet nose tracing a slow cone around the velocity vector, requires both a non-zero SpinMOI and a non-zero spin. Seed spin via :Magnus():SpinVector() or :SixDOF():InitialAngularVelocity().

:Magnus()
    :SpinVector(Vector3.new(0, 0, 500))   -- 500 rad/s right-hand spin
:Done()
:SixDOF()
    :Enabled(true)
    :SpinMOI(0.0003)
    -- ... other required fields
:Done()

The solver computes precession as ω_prec = spinAxis x aeroTorque / H where H = SpinMOI · spinRate. A larger SpinMOI → slower precession; a smaller one → faster cone.

6DOF, tuning guide

Static stability, set PitchingMomentSlope negative (e.g. -0.5). This applies a restoring torque whenever the nose deviates from velocity, keeping the bullet pointing forward. More negative = stiffer.

Damping, set PitchDampingCoeff (e.g. 0.02) to kill wobble. Without damping, aerodynamic torques cause permanent coning. Start at 0.010.05 and increase until the bullet settles within a few frames.

Lift, LiftCoefficientSlope (dCL/dα) scales the Magnus-like lift force proportional to AoA. Typical range 1.04.0. Set to 0 to disable lift entirely and model drag-only nose attitude.

AoA-dependent drag, AoADragFactor multiplies drag by 1 + k·sin²(AoA). 3.0 triples drag when broadside. Useful for tumbling or unstable projectiles.

Roll decay, RollDampingCoeff slowly kills axial spin. Without it, a bullet with SpinVector set maintains its spin forever.

Reference values for a typical rifle bullet:

  • ReferenceArea0.0050.02 studs²
  • ReferenceLength0.030.1 studs
  • MomentOfInertia0.00050.005
  • SpinMOI0.00010.001
  • BulletMass0.0040.015

Builders are reusable, call :Build() multiple times to produce independent frozen tables from the same configuration.

-- Produce two independent frozen tables from the same builder
local RifleBehavior  = RifleBuilder:Build()
local SniperBehavior = RifleBuilder:Physics():MaxDistance(2000):Done():Build()
Presets

Use BehaviorBuilder.Sniper, BehaviorBuilder.Grenade, or BehaviorBuilder.Pistol as a starting point, then chain additional overrides before calling :Build().

Build-time validation

All validation is deferred to :Build() rather than per-setter. This means the builder never throws mid-chain, all errors are collected and reported together when :Build() is called. :Build() returns nil if any error is found.

Functions

new

BehaviorBuilder.new() → BehaviorBuilder

Creates a new builder pre-populated with all default values.

Each call allocates a fresh RaycastParams and reads workspace.Gravity at construction time, builders never share mutable references with each other.

Inherit

BehaviorBuilder.Inherit(
frozenVetraBehavior--

A frozen behavior table produced by :Build().

) → BehaviorBuilder--

Mutable builder pre-populated from the frozen table.

Creates a new BehaviorBuilder pre-populated from a frozen VetraBehavior table, with every field marked dirty.

This is the inverse of :Build(), it lets you round-trip a frozen behavior back into a mutable builder so you can tweak individual fields without reconstructing from scratch.

Because every field is marked dirty, the resulting builder works correctly with :Impose() and :Merge(), all its values are treated as intentional rather than defaults.

-- Received from a registry, config file, or another module
local existing = BehaviorRegistry:Get("Sniper")

-- Round-trip: unfreeze → tweak → refreeze
local tweaked = BehaviorBuilder.Inherit(existing)
    :Physics():MaxDistance(2000):Done()
    :Build()

Note that BehaviorBuilder.Inherit is a static constructor, not an instance method, call it on the class, not on a builder instance.

Sniper

BehaviorBuilder.Sniper() → BehaviorBuilder

Returns a pre-configured builder for a high-velocity, long-range, pierce-capable, high-fidelity projectile. No bouncing.

Preset values: MaxDistance 1500, MinSpeed 50, MaxPierceCount 3, PierceSpeedThreshold 200, PierceSpeedRetention 0.9, PierceNormalBias 0.8, HighFidelitySegmentSize 0.2, HighFidelityFrameBudget 2.

Suitable for rifles and anti-materiel weapons.

Grenade

BehaviorBuilder.Grenade() → BehaviorBuilder

Returns a pre-configured builder for a low-speed, gravity-affected, bouncy projectile with corner-trap detection tuned for tight-space ricochets.

Preset values: MaxDistance 400, MinSpeed 2, MaxBounces 6, BounceSpeedThreshold 10, Restitution 0.55, NormalPerturbation 0.05, CornerTimeThreshold 0.005, CornerDisplacementThreshold 0.3, HighFidelitySegmentSize 0.4.

Suitable for thrown grenades or bouncing explosives.

Pistol

BehaviorBuilder.Pistol() → BehaviorBuilder

Returns a pre-configured builder for a standard short-to-mid range projectile with single pierce and no bounce.

Preset values: MaxDistance 300, MinSpeed 5, MaxPierceCount 1, PierceSpeedThreshold 80, PierceSpeedRetention 0.75.

Suitable for handguns and SMGs.

Physics

BehaviorBuilder:Physics() → PhysicsBuilder

Opens the Physics configuration group.

Available setters: :MaxDistance(), :MaxSpeed(), :MinSpeed(), :Gravity(), :Acceleration(), :RaycastParams(), :CastFunction(), :BulletMass(). Call :Done() to return to the root builder.

Homing

BehaviorBuilder:Homing() → HomingBuilder

Opens the Homing configuration group.

Available setters: :Filter(), :PositionProvider(), :Strength(), :MaxDuration(), :AcquisitionRadius(). Call :Done() to return.

Pierce

BehaviorBuilder:Pierce() → PierceBuilder

Opens the Pierce configuration group.

Available setters: :Filter(), :Max(), :SpeedThreshold(), :SpeedRetention(), :NormalBias(), :PierceDepth(), :PierceForce(), :ThicknessLimit(). Call :Done() to return.

CAUTION

Pierce and bounce are mutually exclusive per hit. Pierce is evaluated first.

Bounce

BehaviorBuilder:Bounce() → BounceBuilder

Opens the Bounce configuration group.

Available setters: :Filter(), :Max(), :SpeedThreshold(), :Restitution(), :MaterialRestitution(), :NormalPerturbation(), :ResetPierceOnBounce(). Call :Done() to return.

CAUTION

Pierce and bounce are mutually exclusive per hit. Bounce is only evaluated if pierce did not occur.

HighFidelity

BehaviorBuilder:HighFidelity() → HighFidelityBuilder

Opens the HighFidelity configuration group.

Available setters: :SegmentSize(), :FrameBudget(), :AdaptiveScale(), :MinSegmentSize(), :MaxBouncesPerFrame(). Call :Done() to return.

CornerTrap

BehaviorBuilder:CornerTrap() → CornerTrapBuilder

Opens the CornerTrap configuration group.

Available setters: :TimeThreshold(), :PositionHistorySize(), :DisplacementThreshold(), :EMAAlpha(), :EMAThreshold(), :MinProgressPerBounce(). Call :Done() to return.

Cosmetic

BehaviorBuilder:Cosmetic() → CosmeticBuilder

Opens the Cosmetic configuration group.

Available setters: :Template(), :Container(), :Provider(). Call :Done() to return.

CAUTION

:Provider() and :Template() are mutually exclusive. Provider takes priority if both are set, and a warning is logged.

Debug

BehaviorBuilder:Debug() → DebugBuilder

Opens the Debug configuration group.

Available setters: :Visualize(). Call :Done() to return.

Drag

BehaviorBuilder:Drag() → DragBuilder

Opens the Drag configuration group.

Available setters: :Coefficient(), :Model(), :SegmentInterval(), :CustomMachTable(). Call :Done() to return.

CAUTION

:CustomMachTable() is required when Model = Vetra.Enums.DragModel.Custom. :Build() returns nil if it is omitted.

Wind

BehaviorBuilder:Wind() → WindBuilder

Opens the Wind configuration group.

Available setters: :Response(). Call :Done() to return.

Response is a multiplier on the solver's global wind vector set via Vetra:SetWind. 1.0 = fully affected, 0.0 = immune.

Magnus

BehaviorBuilder:Magnus() → MagnusBuilder

Opens the Magnus configuration group.

Available setters: :SpinVector(), :Coefficient(), :SpinDecayRate(). Call :Done() to return.

Start small

MagnusCoefficient is highly sensitive. Start at 0.00005 and increase incrementally, 0.0001 already produces visible drift at typical speeds.

GyroDrift

BehaviorBuilder:GyroDrift() → GyroDriftBuilder

Opens the GyroDrift configuration group.

Available setters: :Rate(), :Axis(). Call :Done() to return.

Setting :Rate() enables drift. Axis defaults to world UP (right-hand rifling) when not set.

Tumble

BehaviorBuilder:Tumble() → TumbleBuilder

Opens the Tumble configuration group.

Available setters: :SpeedThreshold(), :DragMultiplier(), :LateralStrength(), :OnPierce(), :RecoverySpeed(). Call :Done() to return.

CAUTION

:RecoverySpeed() must be greater than :SpeedThreshold() if both are set. :Build() enforces this constraint.

Fragmentation

BehaviorBuilder:Fragmentation() → FragmentationBuilder

Opens the Fragmentation configuration group.

Available setters: :OnPierce(), :Count(), :Deviation(). Call :Done() to return.

Each fragment is a fully live cast. It fires OnHit independently and can bounce, pierce, and apply drag if its inherited behavior includes those.

SpeedProfiles

BehaviorBuilder:SpeedProfiles() → SpeedProfilesBuilder

Opens the SpeedProfiles configuration group.

Available setters: :Thresholds(), :Supersonic(), :Subsonic(). :Supersonic() and :Subsonic() each return a SpeedProfileBuilder. Call :Done() on each profile builder to return to SpeedProfilesBuilder, then :Done() again to return to the root builder.

:SpeedProfiles()
    :Thresholds({ 343 })
    :Supersonic()
        :DragCoefficient(0.0015)
    :Done()
    :Subsonic()
        :DragCoefficient(0.004)
        :NormalPerturbation(0.06)
    :Done()
:Done()

Trajectory

BehaviorBuilder:Trajectory() → TrajectoryBuilder

Opens the Trajectory configuration group.

Available setters: :Provider(). Call :Done() to return.

Provider overrides bullet position each frame. Return nil from the callback to end the override and terminate the cast. Signature: (elapsed: number) -> Vector3?

LOD

BehaviorBuilder:LOD() → LODBuilder

Opens the LOD configuration group.

Available setters: :Distance(). Call :Done() to return.

Bullets beyond Distance studs from the LOD origin step at reduced frequency. 0 disables LOD for this cast.

SixDOF

BehaviorBuilder:SixDOF() → SixDOFBuilder

Opens the SixDOF configuration group.

Available setters: :Enabled(), :LiftCoefficientSlope(), :PitchingMomentSlope(), :PitchDampingCoeff(), :RollDampingCoeff(), :AoADragFactor(), :ReferenceArea(), :ReferenceLength(), :AirDensity(), :MomentOfInertia(), :SpinMOI(), :MaxAngularSpeed(), :InitialOrientation(), :InitialAngularVelocity(). Call :Done() to return.

All fields are ignored unless :Enabled(true) is set. When enabled, BulletMass, ReferenceArea, ReferenceLength, and MomentOfInertia are required, :Build() returns nil if any are zero.

BulletMass required

Set mass via :Physics():BulletMass() before enabling 6DOF. The solver converts aerodynamic force vectors into accelerations using a = F / m, zero mass causes a division by zero.

BatchTravel

BehaviorBuilder:BatchTravel(valueboolean) → BehaviorBuilder

Enables or disables batch travel for this cast. When true, travel events go to OnTravelBatch instead of individual OnTravel fires.

Default: false

Hitscan

BehaviorBuilder:Hitscan(valueboolean) → BehaviorBuilder

Enables or disables hitscan mode for this cast.

When true, the entire bullet path, pierce, bounce, and all signals — resolves synchronously inside Vetra:Fire. No per-frame physics stepping occurs: gravity, drag, Magnus, and all kinematic forces are skipped. The bullet travels in straight lines between bounces.

All signals (OnHit, OnBounce, OnPierce, OnTerminated) fire in the normal order before Fire() returns.

Default: false

No physics forces

DragCoefficient, SpinVector, MagnusCoefficient, gravity, and homing do not apply to hitscan casts. For fast projectiles that still need physics, increase speed and reduce MaxDistance instead.

Clone

BehaviorBuilder:Clone() → BehaviorBuilder--

Independent copy with cloned config and dirty set.

Returns an independent BehaviorBuilder whose configuration and dirty set are deep copies of this builder's. Changes to either builder after cloning do not affect the other.

Use this to derive variants from a shared archetype without mutating it:

local Base    = BehaviorBuilder.Sniper()
local Variant = Base:Clone():Physics():MaxDistance(2000):Done():Build()
-- Base is unchanged; Variant has MaxDistance = 2000

:Clone() is the correct way to branch from a preset. Calling setters directly on the preset builder mutates it for all future :Build() calls, which is rarely what you want.

Impose

BehaviorBuilder:Impose(
otherBehaviorBuilder--

The modifier to apply. Must be a BehaviorBuilder.

) → BehaviorBuilder--

self, for chaining.

Copies only the explicitly-set fields from other onto this builder.

"Explicitly set" means a field whose setter was called on other, tracked internally via dirty flags. Fields sitting at their defaults on other are never copied, so a modifier cannot silently clobber values it never touched.

Returns self for chaining. Does not mutate other.

-- Define a reusable modifier, only two fields are dirty.
local APMod = BehaviorBuilder.new()
    :Pierce()
        :Max(5)
        :SpeedRetention(0.95)
    :Done()

-- Apply to any base without touching MaxDistance, HighFidelity, etc.
local APSniper = BehaviorBuilder.Sniper():Clone():Impose(APMod):Build()
local APPistol = BehaviorBuilder.Pistol():Clone():Impose(APMod):Build()

Modifiers stack cleanly, each :Impose() only writes its own dirty set:

local HollowMod = BehaviorBuilder.new()
    :Tumble():OnPierce(true):DragMultiplier(5):Done()

local APHollow = BehaviorBuilder.Sniper():Clone()
    :Impose(APMod)
    :Impose(HollowMod)
    :Build()
Last write wins

If two modifiers set the same field, the second :Impose() wins. There is no merge strategy for conflicting values, ordering is the caller's responsibility.

Merge

BehaviorBuilder:Merge(
...BehaviorBuilder--

One or more modifier builders to apply in order.

) → BehaviorBuilder--

New independent builder with all modifiers imposed.

Returns a new builder that is a clone of self with all provided modifiers applied in order via :Impose(). Neither self nor any modifier is mutated.

Equivalent to self:Clone():Impose(a):Impose(b):..., but reads more naturally when combining a preset with modifiers at the call site.

local Behavior = BehaviorBuilder.Sniper()
    :Merge(APMod, HollowMod)
    :Build()

Because :Merge() returns a builder, you can continue chaining after it:

local Behavior = BehaviorBuilder.Sniper()
    :Merge(APMod)
    :Physics():MaxDistance(2000):Done()   -- applied after the merge
    :Build()

When

BehaviorBuilder:When(
conditionany,--

Truthy value to gate the block. Falsy = skip.

fn(builderBehaviorBuilder) → ()--

Block to apply if condition is truthy.

) → BehaviorBuilder--

self, for chaining.

Conditionally applies a block of builder calls without breaking the fluent chain. If condition is falsy the builder is returned unchanged.

The callback receives self and is called for its side effects, it should not return a value.

local Behavior = BehaviorBuilder.Sniper()
    :When(isRaining,   function(b) b:Wind():Response(1.5):Done() end)
    :When(isHeavyAmmo, function(b) b:Pierce():Max(5):Done() end)
    :When(isDebug,     function(b) b:Debug():Visualize(true):Done() end)
    :Build()

Without :When(), each conditional would require breaking out of the chain:

local b = BehaviorBuilder.Sniper()
if isRaining   then b:Wind():Response(1.5):Done() end
if isHeavyAmmo then b:Pierce():Max(5):Done() end
if isDebug     then b:Debug():Visualize(true):Done() end
local Behavior = b:Build()

Both are equivalent. :When() is purely ergonomic, it keeps construction as a single coherent declaration.

Build

BehaviorBuilder:Build() → VetraBehavior?--

Frozen behavior table, or nil if validation failed.

Validates the current configuration and returns a frozen VetraBehavior table ready to pass to Vetra:Fire.

All validation errors are collected and logged together so every problem is reported at once. Returns nil if any validation error is found.

Does not consume the builder, call :Build() multiple times to produce independent frozen tables from the same configuration.

local RifleBehavior  = RifleBuilder:Build()
local SniperBehavior = RifleBuilder:Physics():MaxDistance(2000):Done():Build()
Show raw api
{
    "functions": [
        {
            "name": "new",
            "desc": "Creates a new builder pre-populated with all default values.\n\nEach call allocates a fresh `RaycastParams` and reads `workspace.Gravity`\nat construction time, builders never share mutable references with\neach other.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 194,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Physics",
            "desc": "Opens the Physics configuration group.\n\nAvailable setters: `:MaxDistance()`, `:MaxSpeed()`, `:MinSpeed()`,\n`:Gravity()`, `:Acceleration()`, `:RaycastParams()`, `:CastFunction()`,\n`:BulletMass()`. Call `:Done()` to return to the root builder.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "PhysicsBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 207,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Homing",
            "desc": "Opens the Homing configuration group.\n\nAvailable setters: `:Filter()`, `:PositionProvider()`, `:Strength()`,\n`:MaxDuration()`, `:AcquisitionRadius()`. Call `:Done()` to return.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "HomingBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 217,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Pierce",
            "desc": "Opens the Pierce configuration group.\n\nAvailable setters: `:Filter()`, `:Max()`, `:SpeedThreshold()`,\n`:SpeedRetention()`, `:NormalBias()`, `:PierceDepth()`,\n`:PierceForce()`, `:ThicknessLimit()`. Call `:Done()` to return.\n\n:::caution\nPierce and bounce are mutually exclusive per hit. Pierce is evaluated first.\n:::",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "PierceBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 232,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Bounce",
            "desc": "Opens the Bounce configuration group.\n\nAvailable setters: `:Filter()`, `:Max()`, `:SpeedThreshold()`,\n`:Restitution()`, `:MaterialRestitution()`, `:NormalPerturbation()`,\n`:ResetPierceOnBounce()`. Call `:Done()` to return.\n\n:::caution\nPierce and bounce are mutually exclusive per hit. Bounce is only evaluated\nif pierce did not occur.\n:::",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "BounceBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 248,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "HighFidelity",
            "desc": "Opens the HighFidelity configuration group.\n\nAvailable setters: `:SegmentSize()`, `:FrameBudget()`, `:AdaptiveScale()`,\n`:MinSegmentSize()`, `:MaxBouncesPerFrame()`. Call `:Done()` to return.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "HighFidelityBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 258,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "CornerTrap",
            "desc": "Opens the CornerTrap configuration group.\n\nAvailable setters: `:TimeThreshold()`, `:PositionHistorySize()`,\n`:DisplacementThreshold()`, `:EMAAlpha()`, `:EMAThreshold()`,\n`:MinProgressPerBounce()`. Call `:Done()` to return.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "CornerTrapBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 269,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Cosmetic",
            "desc": "Opens the Cosmetic configuration group.\n\nAvailable setters: `:Template()`, `:Container()`, `:Provider()`.\nCall `:Done()` to return.\n\n:::caution\n`:Provider()` and `:Template()` are mutually exclusive. Provider takes\npriority if both are set, and a warning is logged.\n:::",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "CosmeticBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 284,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Debug",
            "desc": "Opens the Debug configuration group.\n\nAvailable setters: `:Visualize()`. Call `:Done()` to return.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "DebugBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 293,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Drag",
            "desc": "Opens the Drag configuration group.\n\nAvailable setters: `:Coefficient()`, `:Model()`, `:SegmentInterval()`,\n`:CustomMachTable()`. Call `:Done()` to return.\n\n:::caution\n`:CustomMachTable()` is required when `Model = Vetra.Enums.DragModel.Custom`.\n`:Build()` returns `nil` if it is omitted.\n:::",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "DragBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 308,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Wind",
            "desc": "Opens the Wind configuration group.\n\nAvailable setters: `:Response()`. Call `:Done()` to return.\n\n`Response` is a multiplier on the solver's global wind vector set via\n`Vetra:SetWind`. `1.0` = fully affected, `0.0` = immune.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "WindBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 320,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Magnus",
            "desc": "Opens the Magnus configuration group.\n\nAvailable setters: `:SpinVector()`, `:Coefficient()`, `:SpinDecayRate()`.\nCall `:Done()` to return.\n\n:::caution Start small\n`MagnusCoefficient` is highly sensitive. Start at `0.00005` and increase\nincrementally, `0.0001` already produces visible drift at typical speeds.\n:::",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "MagnusBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 335,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "GyroDrift",
            "desc": "Opens the GyroDrift configuration group.\n\nAvailable setters: `:Rate()`, `:Axis()`. Call `:Done()` to return.\n\nSetting `:Rate()` enables drift. `Axis` defaults to world UP\n(right-hand rifling) when not set.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "GyroDriftBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 347,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Tumble",
            "desc": "Opens the Tumble configuration group.\n\nAvailable setters: `:SpeedThreshold()`, `:DragMultiplier()`,\n`:LateralStrength()`, `:OnPierce()`, `:RecoverySpeed()`.\nCall `:Done()` to return.\n\n:::caution\n`:RecoverySpeed()` must be greater than `:SpeedThreshold()` if both are\nset. `:Build()` enforces this constraint.\n:::",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "TumbleBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 363,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Fragmentation",
            "desc": "Opens the Fragmentation configuration group.\n\nAvailable setters: `:OnPierce()`, `:Count()`, `:Deviation()`.\nCall `:Done()` to return.\n\nEach fragment is a fully live cast. It fires `OnHit` independently and\ncan bounce, pierce, and apply drag if its inherited behavior includes those.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "FragmentationBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 376,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "SpeedProfiles",
            "desc": "Opens the SpeedProfiles configuration group.\n\nAvailable setters: `:Thresholds()`, `:Supersonic()`, `:Subsonic()`.\n`:Supersonic()` and `:Subsonic()` each return a [SpeedProfileBuilder].\nCall `:Done()` on each profile builder to return to [SpeedProfilesBuilder],\nthen `:Done()` again to return to the root builder.\n\n```lua\n:SpeedProfiles()\n    :Thresholds({ 343 })\n    :Supersonic()\n        :DragCoefficient(0.0015)\n    :Done()\n    :Subsonic()\n        :DragCoefficient(0.004)\n        :NormalPerturbation(0.06)\n    :Done()\n:Done()\n```",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "SpeedProfilesBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 401,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Trajectory",
            "desc": "Opens the Trajectory configuration group.\n\nAvailable setters: `:Provider()`. Call `:Done()` to return.\n\n`Provider` overrides bullet position each frame. Return `nil` from the\ncallback to end the override and terminate the cast.\nSignature: `(elapsed: number) -> Vector3?`",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "TrajectoryBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 414,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "LOD",
            "desc": "Opens the LOD configuration group.\n\nAvailable setters: `:Distance()`. Call `:Done()` to return.\n\nBullets beyond `Distance` studs from the LOD origin step at reduced\nfrequency. `0` disables LOD for this cast.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "LODBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 426,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "SixDOF",
            "desc": "Opens the SixDOF configuration group.\n\nAvailable setters: `:Enabled()`, `:LiftCoefficientSlope()`,\n`:PitchingMomentSlope()`, `:PitchDampingCoeff()`, `:RollDampingCoeff()`,\n`:AoADragFactor()`, `:ReferenceArea()`, `:ReferenceLength()`,\n`:AirDensity()`, `:MomentOfInertia()`, `:SpinMOI()`,\n`:MaxAngularSpeed()`, `:InitialOrientation()`, `:InitialAngularVelocity()`.\nCall `:Done()` to return.\n\nAll fields are ignored unless `:Enabled(true)` is set. When enabled,\n`BulletMass`, `ReferenceArea`, `ReferenceLength`, and `MomentOfInertia`\nare required, `:Build()` returns `nil` if any are zero.\n\n:::caution BulletMass required\nSet mass via `:Physics():BulletMass()` before enabling 6DOF.\nThe solver converts aerodynamic force vectors into accelerations using\n`a = F / m`, zero mass causes a division by zero.\n:::",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "SixDOFBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 450,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "BatchTravel",
            "desc": "Enables or disables batch travel for this cast. When `true`, travel\nevents go to `OnTravelBatch` instead of individual `OnTravel` fires.\n\nDefault: `false`",
            "params": [
                {
                    "name": "value",
                    "desc": "",
                    "lua_type": "boolean"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 461,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Hitscan",
            "desc": "Enables or disables hitscan mode for this cast.\n\nWhen `true`, the entire bullet path, pierce, bounce, and all signals —\nresolves synchronously inside [Vetra:Fire]. No per-frame physics stepping\noccurs: gravity, drag, Magnus, and all kinematic forces are skipped.\nThe bullet travels in straight lines between bounces.\n\nAll signals (`OnHit`, `OnBounce`, `OnPierce`, `OnTerminated`) fire in the\nnormal order before `Fire()` returns.\n\nDefault: `false`\n\n:::caution No physics forces\n`DragCoefficient`, `SpinVector`, `MagnusCoefficient`, gravity, and homing\ndo not apply to hitscan casts. For fast projectiles that still need physics,\nincrease speed and reduce `MaxDistance` instead.\n:::",
            "params": [
                {
                    "name": "value",
                    "desc": "",
                    "lua_type": "boolean"
                }
            ],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 485,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Clone",
            "desc": "Returns an independent `BehaviorBuilder` whose configuration and dirty set\nare deep copies of this builder's. Changes to either builder after cloning\ndo not affect the other.\n\nUse this to derive variants from a shared archetype without mutating it:\n\n```lua\nlocal Base    = BehaviorBuilder.Sniper()\nlocal Variant = Base:Clone():Physics():MaxDistance(2000):Done():Build()\n-- Base is unchanged; Variant has MaxDistance = 2000\n```\n\n`:Clone()` is the correct way to branch from a preset. Calling setters\ndirectly on the preset builder mutates it for all future `:Build()` calls,\nwhich is rarely what you want.",
            "params": [],
            "returns": [
                {
                    "desc": "Independent copy with cloned config and dirty set.",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 508,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Impose",
            "desc": "Copies only the **explicitly-set** fields from `other` onto this builder.\n\n\"Explicitly set\" means a field whose setter was called on `other`, tracked\ninternally via dirty flags. Fields sitting at their defaults on `other` are\nnever copied, so a modifier cannot silently clobber values it never touched.\n\nReturns `self` for chaining. Does not mutate `other`.\n\n```lua\n-- Define a reusable modifier, only two fields are dirty.\nlocal APMod = BehaviorBuilder.new()\n    :Pierce()\n        :Max(5)\n        :SpeedRetention(0.95)\n    :Done()\n\n-- Apply to any base without touching MaxDistance, HighFidelity, etc.\nlocal APSniper = BehaviorBuilder.Sniper():Clone():Impose(APMod):Build()\nlocal APPistol = BehaviorBuilder.Pistol():Clone():Impose(APMod):Build()\n```\n\nModifiers stack cleanly, each `:Impose()` only writes its own dirty set:\n\n```lua\nlocal HollowMod = BehaviorBuilder.new()\n    :Tumble():OnPierce(true):DragMultiplier(5):Done()\n\nlocal APHollow = BehaviorBuilder.Sniper():Clone()\n    :Impose(APMod)\n    :Impose(HollowMod)\n    :Build()\n```\n\n:::caution Last write wins\nIf two modifiers set the same field, the second `:Impose()` wins.\nThere is no merge strategy for conflicting values, ordering is the\ncaller's responsibility.\n:::",
            "params": [
                {
                    "name": "other",
                    "desc": "The modifier to apply. Must be a BehaviorBuilder.",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "returns": [
                {
                    "desc": "self, for chaining.",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 553,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Merge",
            "desc": "Returns a new builder that is a clone of `self` with all provided modifiers\napplied in order via `:Impose()`. Neither `self` nor any modifier is mutated.\n\nEquivalent to `self:Clone():Impose(a):Impose(b):...`, but reads more\nnaturally when combining a preset with modifiers at the call site.\n\n```lua\nlocal Behavior = BehaviorBuilder.Sniper()\n    :Merge(APMod, HollowMod)\n    :Build()\n```\n\nBecause `:Merge()` returns a builder, you can continue chaining after it:\n\n```lua\nlocal Behavior = BehaviorBuilder.Sniper()\n    :Merge(APMod)\n    :Physics():MaxDistance(2000):Done()   -- applied after the merge\n    :Build()\n```",
            "params": [
                {
                    "name": "...",
                    "desc": "One or more modifier builders to apply in order.",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "returns": [
                {
                    "desc": "New independent builder with all modifiers imposed.",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 582,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Inherit",
            "desc": "Creates a new `BehaviorBuilder` pre-populated from a frozen `VetraBehavior`\ntable, with every field marked dirty.\n\nThis is the inverse of `:Build()`, it lets you round-trip a frozen behavior\nback into a mutable builder so you can tweak individual fields without\nreconstructing from scratch.\n\nBecause every field is marked dirty, the resulting builder works correctly\nwith `:Impose()` and `:Merge()`, all its values are treated as intentional\nrather than defaults.\n\n```lua\n-- Received from a registry, config file, or another module\nlocal existing = BehaviorRegistry:Get(\"Sniper\")\n\n-- Round-trip: unfreeze → tweak → refreeze\nlocal tweaked = BehaviorBuilder.Inherit(existing)\n    :Physics():MaxDistance(2000):Done()\n    :Build()\n```\n\nNote that `BehaviorBuilder.Inherit` is a **static constructor**, not an\ninstance method, call it on the class, not on a builder instance.",
            "params": [
                {
                    "name": "frozen",
                    "desc": "A frozen behavior table produced by `:Build()`.",
                    "lua_type": "VetraBehavior"
                }
            ],
            "returns": [
                {
                    "desc": "Mutable builder pre-populated from the frozen table.",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 612,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "When",
            "desc": "Conditionally applies a block of builder calls without breaking the fluent\nchain. If `condition` is falsy the builder is returned unchanged.\n\nThe callback receives `self` and is called for its side effects, it should\nnot return a value.\n\n```lua\nlocal Behavior = BehaviorBuilder.Sniper()\n    :When(isRaining,   function(b) b:Wind():Response(1.5):Done() end)\n    :When(isHeavyAmmo, function(b) b:Pierce():Max(5):Done() end)\n    :When(isDebug,     function(b) b:Debug():Visualize(true):Done() end)\n    :Build()\n```\n\nWithout `:When()`, each conditional would require breaking out of the chain:\n\n```lua\nlocal b = BehaviorBuilder.Sniper()\nif isRaining   then b:Wind():Response(1.5):Done() end\nif isHeavyAmmo then b:Pierce():Max(5):Done() end\nif isDebug     then b:Debug():Visualize(true):Done() end\nlocal Behavior = b:Build()\n```\n\nBoth are equivalent. `:When()` is purely ergonomic, it keeps construction\nas a single coherent declaration.",
            "params": [
                {
                    "name": "condition",
                    "desc": "Truthy value to gate the block. Falsy = skip.",
                    "lua_type": "any"
                },
                {
                    "name": "fn",
                    "desc": "Block to apply if condition is truthy.",
                    "lua_type": "(builder: BehaviorBuilder) -> ()"
                }
            ],
            "returns": [
                {
                    "desc": "self, for chaining.",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 646,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Build",
            "desc": "Validates the current configuration and returns a frozen `VetraBehavior`\ntable ready to pass to [Vetra:Fire].\n\nAll validation errors are collected and logged together so every problem\nis reported at once. Returns `nil` if any validation error is found.\n\nDoes **not** consume the builder, call `:Build()` multiple times to\nproduce independent frozen tables from the same configuration.\n\n```lua\nlocal RifleBehavior  = RifleBuilder:Build()\nlocal SniperBehavior = RifleBuilder:Physics():MaxDistance(2000):Done():Build()\n```",
            "params": [],
            "returns": [
                {
                    "desc": "Frozen behavior table, or nil if validation failed.",
                    "lua_type": "VetraBehavior?"
                }
            ],
            "function_type": "method",
            "source": {
                "line": 667,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Sniper",
            "desc": "Returns a pre-configured builder for a high-velocity, long-range,\npierce-capable, high-fidelity projectile. No bouncing.\n\n**Preset values:** MaxDistance 1500, MinSpeed 50, MaxPierceCount 3,\nPierceSpeedThreshold 200, PierceSpeedRetention 0.9, PierceNormalBias 0.8,\nHighFidelitySegmentSize 0.2, HighFidelityFrameBudget 2.\n\nSuitable for rifles and anti-materiel weapons.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 683,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Grenade",
            "desc": "Returns a pre-configured builder for a low-speed, gravity-affected,\nbouncy projectile with corner-trap detection tuned for tight-space ricochets.\n\n**Preset values:** MaxDistance 400, MinSpeed 2, MaxBounces 6,\nBounceSpeedThreshold 10, Restitution 0.55, NormalPerturbation 0.05,\nCornerTimeThreshold 0.005, CornerDisplacementThreshold 0.3,\nHighFidelitySegmentSize 0.4.\n\nSuitable for thrown grenades or bouncing explosives.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 698,
                "path": "docs/BehaviorBuilder.lua"
            }
        },
        {
            "name": "Pistol",
            "desc": "Returns a pre-configured builder for a standard short-to-mid range\nprojectile with single pierce and no bounce.\n\n**Preset values:** MaxDistance 300, MinSpeed 5, MaxPierceCount 1,\nPierceSpeedThreshold 80, PierceSpeedRetention 0.75.\n\nSuitable for handguns and SMGs.",
            "params": [],
            "returns": [
                {
                    "desc": "",
                    "lua_type": "BehaviorBuilder"
                }
            ],
            "function_type": "static",
            "source": {
                "line": 711,
                "path": "docs/BehaviorBuilder.lua"
            }
        }
    ],
    "properties": [],
    "types": [],
    "name": "BehaviorBuilder",
    "desc": "Fluent typed configuration builder for [Vetra].\n\nInstead of constructing raw behavior tables by hand, chain namespace\nmethods and call `:Build()` to produce a validated, frozen `VetraBehavior`.\n\n```lua\nlocal Behavior = BehaviorBuilder.new()\n    :Physics()\n        :MaxDistance(500)\n        :MinSpeed(5)\n    :Done()\n    :Bounce()\n        :Max(3)\n        :Restitution(0.7)\n        :Filter(function(context, result, vel)\n            return result.Instance:HasTag(\"Bouncy\")\n        end)\n    :Done()\n    :Drag()\n        :Coefficient(0.003)\n        :Model(Vetra.Enums.DragModel.G7)\n    :Done()\n    :Build()\n```\n\n**Namespace overview:**\n\n| Method | Configures |\n|--------|------------|\n| `:Physics()` | MaxDistance, MaxSpeed, MinSpeed, Gravity, Acceleration, RaycastParams, CastFunction, BulletMass |\n| `:Homing()` | Filter, PositionProvider, Strength, MaxDuration, AcquisitionRadius |\n| `:Pierce()` | Filter, Max, SpeedThreshold, SpeedRetention, NormalBias, PierceDepth, PierceForce, ThicknessLimit |\n| `:Bounce()` | Filter, Max, SpeedThreshold, Restitution, MaterialRestitution, NormalPerturbation, ResetPierceOnBounce |\n| `:HighFidelity()` | SegmentSize, FrameBudget, AdaptiveScale, MinSegmentSize, MaxBouncesPerFrame |\n| `:CornerTrap()` | TimeThreshold, PositionHistorySize, DisplacementThreshold, EMAAlpha, EMAThreshold, MinProgressPerBounce |\n| `:Cosmetic()` | Template, Container, Provider |\n| `:Debug()` | Visualize |\n| `:Drag()` | Coefficient, Model, SegmentInterval, CustomMachTable |\n| `:Wind()` | Response |\n| `:Magnus()` | SpinVector, Coefficient, SpinDecayRate |\n| `:GyroDrift()` | Rate, Axis |\n| `:Tumble()` | SpeedThreshold, DragMultiplier, LateralStrength, OnPierce, RecoverySpeed |\n| `:Fragmentation()` | OnPierce, Count, Deviation |\n| `:SpeedProfiles()` | Thresholds, `:Supersonic()` → profile, `:Subsonic()` → profile |\n| `:Trajectory()` | Provider |\n| `:LOD()` | Distance |\n| `:SixDOF()` | Enabled, LiftCoefficientSlope, PitchingMomentSlope, PitchDampingCoeff, RollDampingCoeff, AoADragFactor, ReferenceArea, ReferenceLength, AirDensity, MomentOfInertia, SpinMOI, MaxAngularSpeed, InitialOrientation, InitialAngularVelocity, CLAlphaMachTable, CmAlphaMachTable, CmqMachTable, ClpMachTable |\n| `:BatchTravel()` | Root-level boolean toggle, no sub-builder |\n| `:Hitscan()` | Root-level boolean toggle, no sub-builder |\n| `:Clone()` | Returns an independent copy of this builder |\n| `:Impose(other)` | Copies only the explicitly-set fields from `other` onto self |\n| `:Merge(a, b, ...)` | Clone + impose multiple modifiers, returns new builder |\n| `:When(cond, fn)` | Conditionally apply a block without breaking the chain |\n| `BehaviorBuilder.Inherit(frozen)` | Create a builder from a frozen `VetraBehavior` table |\n\n:::tip DragModel enum\nUse `Vetra.Enums.DragModel` when passing a drag model to `:Drag():Model()`.\nA wrong integer is silently incorrect; an invalid enum key is a nil-index\nwarning immediately at the call site:\n\n```lua\n:Drag():Model(Vetra.Enums.DragModel.G7):Done()\n```\n\nSee [Enums.DragModel] for the full value table and descriptions.\n:::\n\n:::tip 6DOF quick-start\nEnable full six-degrees-of-freedom aerodynamics to give bullets real\npitch, yaw, and roll dynamics driven by lift, pitching moment, damping,\nand gyroscopic precession.\n\n**Minimum required fields when `:SixDOF():Enabled(true)`:**\n\n| Field | Why required |\n|-------|-------------|\n| `BulletMass` (via `:Physics()`) | Converts aerodynamic force vectors into acceleration, F = ma |\n| `ReferenceArea` | Cross-sectional area in studs², scales all aero forces |\n| `ReferenceLength` | Caliber/diameter in studs, scales pitching moment and damping |\n| `MomentOfInertia` | Transverse MOI, governs pitch/yaw angular response |\n\nMinimal example:\n\n```lua\nlocal Behavior = BehaviorBuilder.new()\n    :Physics()\n        :BulletMass(0.01)\n        :MinSpeed(10)\n    :Done()\n    :Drag()\n        :Coefficient(0.003)\n    :Done()\n    :SixDOF()\n        :Enabled(true)\n        :ReferenceArea(0.008)\n        :ReferenceLength(0.05)\n        :MomentOfInertia(0.001)\n        :LiftCoefficientSlope(2.0)\n        :PitchingMomentSlope(-0.5)\n        :PitchDampingCoeff(0.02)\n    :Done()\n    :Build()\n```\n\nAll other 6DOF fields default to safe values (`0` or `false`). Start\nhere and add `AoADragFactor`, `RollDampingCoeff`, and spin fields as needed.\n:::\n\n:::tip 6DOF + gyroscopic precession\nGyroscopic precession, the bullet nose tracing a slow cone around the\nvelocity vector, requires both a non-zero `SpinMOI` and a non-zero spin.\nSeed spin via `:Magnus():SpinVector()` or `:SixDOF():InitialAngularVelocity()`.\n\n```lua\n:Magnus()\n    :SpinVector(Vector3.new(0, 0, 500))   -- 500 rad/s right-hand spin\n:Done()\n:SixDOF()\n    :Enabled(true)\n    :SpinMOI(0.0003)\n    -- ... other required fields\n:Done()\n```\n\nThe solver computes precession as `ω_prec = spinAxis x aeroTorque / H`\nwhere `H = SpinMOI · spinRate`. A larger `SpinMOI` → slower precession;\na smaller one → faster cone.\n:::\n\n:::tip 6DOF, tuning guide\n**Static stability**, set `PitchingMomentSlope` negative (e.g. `-0.5`).\nThis applies a restoring torque whenever the nose deviates from velocity,\nkeeping the bullet pointing forward. More negative = stiffer.\n\n**Damping**, set `PitchDampingCoeff` (e.g. `0.02`) to kill wobble.\nWithout damping, aerodynamic torques cause permanent coning. Start at\n`0.01`–`0.05` and increase until the bullet settles within a few frames.\n\n**Lift**, `LiftCoefficientSlope` (dCL/dα) scales the Magnus-like lift\nforce proportional to AoA. Typical range `1.0`–`4.0`. Set to `0` to\ndisable lift entirely and model drag-only nose attitude.\n\n**AoA-dependent drag**, `AoADragFactor` multiplies drag by `1 + k·sin²(AoA)`.\n`3.0` triples drag when broadside. Useful for tumbling or unstable projectiles.\n\n**Roll decay**, `RollDampingCoeff` slowly kills axial spin. Without it,\na bullet with `SpinVector` set maintains its spin forever.\n\n**Reference values for a typical rifle bullet:**\n- `ReferenceArea` ≈ `0.005`–`0.02` studs²\n- `ReferenceLength` ≈ `0.03`–`0.1` studs\n- `MomentOfInertia` ≈ `0.0005`–`0.005`\n- `SpinMOI` ≈ `0.0001`–`0.001`\n- `BulletMass` ≈ `0.004`–`0.015`\n:::\n\nBuilders are **reusable**, call `:Build()` multiple times to produce\nindependent frozen tables from the same configuration.\n\n```lua\n-- Produce two independent frozen tables from the same builder\nlocal RifleBehavior  = RifleBuilder:Build()\nlocal SniperBehavior = RifleBuilder:Physics():MaxDistance(2000):Done():Build()\n```\n\n:::tip Presets\nUse [BehaviorBuilder.Sniper], [BehaviorBuilder.Grenade], or [BehaviorBuilder.Pistol]\nas a starting point, then chain additional overrides before calling `:Build()`.\n:::\n\n:::caution Build-time validation\nAll validation is deferred to `:Build()` rather than per-setter. This means\nthe builder never throws mid-chain, all errors are collected and reported\ntogether when `:Build()` is called. `:Build()` returns `nil` if any error\nis found.\n:::",
    "source": {
        "line": 181,
        "path": "docs/BehaviorBuilder.lua"
    }
}