‹ Back to My Code VectorFormulas | Advanced Vector3 Mathematics Library
modules lua
-- VectorFormulas.lua
-- Place in ReplicatedStorage as a ModuleScript
-- Categorized Vector3 formulas for Roblox game development.

local V = Vector3.new
local Formulas = {}

-- ============================================================
-- 1. BASIC ARITHMETIC & CONSTRUCTION
-- ============================================================
Formulas.Basic = {
	-- Create a vector
	new          = function(x, y, z) return V(x, y, z) end,
	zero         = function() return V(0, 0, 0) end,
	one          = function() return V(1, 1, 1) end,

	-- Component access
	getX         = function(a) return a.X end,
	getY         = function(a) return a.Y end,
	getZ         = function(a) return a.Z end,

	-- Component-wise min/max/clamp
	min          = function(a, b) return V(math.min(a.X,b.X), math.min(a.Y,b.Y), math.min(a.Z,b.Z)) end,
	max          = function(a, b) return V(math.max(a.X,b.X), math.max(a.Y,b.Y), math.max(a.Z,b.Z)) end,
	clamp        = function(a, lo, hi) return V(
		math.clamp(a.X, lo.X, hi.X),
		math.clamp(a.Y, lo.Y, hi.Y),
		math.clamp(a.Z, lo.Z, hi.Z)
		) end,

	-- Abs / floor / ceil / round component-wise
	abs          = function(a) return V(math.abs(a.X), math.abs(a.Y), math.abs(a.Z)) end,
	floor        = function(a) return V(math.floor(a.X), math.floor(a.Y), math.floor(a.Z)) end,
	ceil         = function(a) return V(math.ceil(a.X), math.ceil(a.Y), math.ceil(a.Z)) end,
	round        = function(a) return V(math.round(a.X), math.round(a.Y), math.round(a.Z)) end,

	-- Sign (-1, 0, 1 per component)
	sign         = function(a) return V(math.sign(a.X), math.sign(a.Y), math.sign(a.Z)) end,

	-- Snap to grid
	snap         = function(a, grid) return V(
		math.floor(a.X / grid + 0.5) * grid,
		math.floor(a.Y / grid + 0.5) * grid,
		math.floor(a.Z / grid + 0.5) * grid
		) end,
	-- Lock size of an instance
	sizelock = function(a: number) return V(a,a,a) end
}

-- ============================================================
-- 2. LENGTH & DISTANCE
-- ============================================================
Formulas.Length = {
	-- Magnitude (length) of a vector
	magnitude    = function(a) return a.Magnitude end,

	-- Squared magnitude (cheaper, avoids sqrt)
	magnitudeSq  = function(a) return a.X*a.X + a.Y*a.Y + a.Z*a.Z end,

	-- Distance between two points
	distance     = function(a, b) return (a - b).Magnitude end,

	-- Squared distance (cheaper for comparisons)
	distanceSq   = function(a, b) local d = a - b; return d.X*d.X + d.Y*d.Y + d.Z*d.Z end,

	-- Manhattan distance (grid-based)
	manhattan    = function(a, b) local d = a - b; return math.abs(d.X) + math.abs(d.Y) + math.abs(d.Z) end,

	-- Horizontal distance (ignore Y)
	horizontal   = function(a, b) return (V(a.X, 0, a.Z) - V(b.X, 0, b.Z)).Magnitude end,

	-- Vertical distance (ignore XZ)
	vertical     = function(a, b) return math.abs(a.Y - b.Y) end,

	-- Check if within range (no sqrt)
	inRange      = function(a, b, range) return Formulas.Length.distanceSq(a, b) <= range * range end,
}

-- ============================================================
-- 3. DIRECTION & NORMALIZATION
-- ============================================================
Formulas.Direction = {
	-- Unit vector pointing from a to b
	fromTo       = function(a, b) return (b - a).Unit end,

	-- Unit vector (normalize)
	normalize    = function(a) return a.Unit end,

	-- Safe normalize (returns zero if magnitude is 0)
	safeUnit     = function(a)
		if a.Magnitude < 1e-6 then return V(0, 0, 0) end
		return a.Unit
	end,

	-- Direction from angle (yaw in radians, on XZ plane)
	fromYaw      = function(yaw) return V(-math.sin(yaw), 0, -math.cos(yaw)) end,

	-- Direction from yaw and pitch (radians)
	fromAngles   = function(yaw, pitch) return V(
		-math.sin(yaw) * math.cos(pitch),
		math.sin(pitch),
		-math.cos(yaw) * math.cos(pitch)
		) end,

	-- Yaw of a direction (radians)
	toYaw        = function(dir) return math.atan2(-dir.X, -dir.Z) end,

	-- Pitch of a direction (radians)
	toPitch      = function(dir) return math.asin(dir.Unit.Y) end,

	-- Reflect a vector off a surface normal
	reflect      = function(dir, normal) return dir - 2 * dir:Dot(normal) * normal end,

	-- Project vector a onto vector b
	project      = function(a, b) return b * (a:Dot(b) / b:Dot(b)) end,

	-- Reject (component of a perpendicular to b)
	reject       = function(a, b) return a - Formulas.Direction.project(a, b) end,

	-- Slide along a surface (remove normal component)
	slide        = function(dir, normal) return dir - normal * dir:Dot(normal) end,

	-- Rotate a vector around an axis by angle (radians)
	rotateAround = function(vec, axis, angle)
		axis = axis.Unit
		local cos = math.cos(angle)
		local sin = math.sin(angle)
		return vec * cos + axis:Cross(vec) * sin + axis * axis:Dot(vec) * (1 - cos)
	end,

	-- Rotate around Y axis (yaw)
	rotateY      = function(vec, angle)
		local cos = math.cos(angle)
		local sin = math.sin(angle)
		return V(vec.X * cos + vec.Z * sin, vec.Y, -vec.X * sin + vec.Z * cos)
	end,
}

-- ============================================================
-- 4. DOT & CROSS PRODUCT
-- ============================================================
Formulas.Products = {
	-- Dot product (angle relation, projection)
	dot          = function(a, b) return a:Dot(b) end,

	-- Cross product (perpendicular vector, torque)
	cross        = function(a, b) return a:Cross(b) end,

	-- Angle between two vectors (radians)
	angle        = function(a, b) return math.acos(math.clamp(a.Unit:Dot(b.Unit), -1, 1)) end,

	-- Angle between two vectors (degrees)
	angleDeg     = function(a, b) return math.deg(Formulas.Products.angle(a, b)) end,

	-- Signed angle around an axis (radians)
	signedAngle  = function(a, b, axis)
		local cross = a:Cross(b)
		local dot   = a:Dot(b)
		local sign  = math.sign(cross:Dot(axis))
		return math.atan2(cross.Magnitude * sign, dot)
	end,

	-- Check if two vectors are perpendicular
	isPerp       = function(a, b) return math.abs(a:Dot(b)) < 1e-6 end,

	-- Check if two vectors are parallel
	isParallel   = function(a, b) return a:Cross(b).Magnitude < 1e-6 end,

	-- Triple product (volume of parallelepiped)
	triple       = function(a, b, c) return a:Dot(b:Cross(c)) end,
}

-- ============================================================
-- 5. INTERPOLATION & BLENDING
-- ============================================================
Formulas.Interpolation = {
	-- Linear interpolation
	lerp         = function(a, b, t) return a:Lerp(b, t) end,

	-- Lerp with clamped t
	lerpClamped  = function(a, b, t) return a:Lerp(b, math.clamp(t, 0, 1)) end,

	-- Smoothstep (ease in/out)
	smoothstep   = function(a, b, t)
		t = math.clamp(t, 0, 1)
		t = t * t * (3 - 2 * t)
		return a:Lerp(b, t)
	end,

	-- Smootherstep (Ken Perlin's improved version)
	smootherstep = function(a, b, t)
		t = math.clamp(t, 0, 1)
		t = t * t * t * (t * (t * 6 - 15) + 10)
		return a:Lerp(b, t)
	end,

	-- Ease in (slow start)
	easeIn       = function(a, b, t) return a:Lerp(b, t * t) end,

	-- Ease out (slow end)
	easeOut      = function(a, b, t) return a:Lerp(b, 1 - (1 - t) * (1 - t)) end,

	-- Exponential smoothing (frame-rate independent)
	-- speed: higher = faster snap
	expSmooth    = function(current, target, speed, dt)
		local alpha = 1 - math.exp(-speed * dt)
		return current:Lerp(target, alpha)
	end,

	-- Spring (overshoots then settles)
	-- Requires velocity state stored externally
	spring       = function(current, target, velocity, stiffness, damping, dt)
		local force = (target - current) * stiffness
		local damp  = velocity * damping
		velocity = velocity + (force - damp) * dt
		return current + velocity * dt, velocity
	end,

	-- Move toward target by a max distance
	moveTowards  = function(current, target, maxDelta)
		local diff = target - current
		local dist = diff.Magnitude
		if dist <= maxDelta or dist < 1e-6 then return target end
		return current + diff.Unit * maxDelta
	end,

	-- Bezier curve (3 control points, quadratic)
	quadBezier   = function(p0, p1, p2, t)
		local u = 1 - t
		return p0 * (u * u) + p1 * (2 * u * t) + p2 * (t * t)
	end,

	-- Bezier curve (4 control points, cubic)
	cubicBezier  = function(p0, p1, p2, p3, t)
		local u = 1 - t
		return p0 * (u*u*u) + p1 * (3*u*u*t) + p2 * (3*u*t*t) + p3 * (t*t*t)
	end,

	-- Catmull-Rom spline (smooth through points)
	catmullRom   = function(p0, p1, p2, p3, t)
		local t2 = t * t
		local t3 = t2 * t
		return (p1 * 2 + (p2 - p0) * t + (p0 * 2 - p1 * 5 + p2 * 4 - p3) * t2 + (p1 * 3 - p0 - p2 * 3 + p3) * t3) * 0.5
	end,
}

-- ============================================================
-- 6. SPATIAL RELATIONSHIPS
-- ============================================================
Formulas.Spatial = {
	-- Midpoint between two points
	midpoint     = function(a, b) return (a + b) * 0.5 end,

	-- Centroid (average) of a list of points
	centroid     = function(points)
		local sum = V(0, 0, 0)
		for _, p in ipairs(points) do sum += p end
		return sum / #points
	end,

	-- Barycenter with weights
	barycenter   = function(points, weights)
		local sum = V(0, 0, 0)
		local total = 0
		for i, p in ipairs(points) do
			sum += p * weights[i]
			total += weights[i]
		end
		return sum / total
	end,

	-- Offset in a direction by distance
	offset       = function(pos, dir, dist) return pos + dir.Unit * dist end,

	-- Point along a line from a to b at t
	alongLine    = function(a, b, t) return a + (b - a) * t end,

	-- Closest point on a line segment to a point
	closestOnSegment = function(p, a, b)
		local ab = b - a
		local t = math.clamp((p - a):Dot(ab) / ab:Dot(ab), 0, 1)
		return a + ab * t
	end,

	-- Closest point on an infinite line to a point
	closestOnLine = function(p, a, b)
		local ab = b - a
		local t = (p - a):Dot(ab) / ab:Dot(ab)
		return a + ab * t
	end,

	-- Perpendicular vector on the XZ plane
	perpXZ       = function(dir) return V(-dir.Z, 0, dir.X) end,

	-- Perpendicular vector in 3D (arbitrary but consistent)
	perp3D       = function(dir)
		local up = math.abs(dir.Y) < 0.99 and V(0, 1, 0) or V(1, 0, 0)
		return dir:Cross(up).Unit
	end,

	-- Random point in a sphere
	randomInSphere = function(center, radius)
		local dir = V(
			math.random() * 2 - 1,
			math.random() * 2 - 1,
			math.random() * 2 - 1
		)
		if dir.Magnitude < 1e-6 then return center end
		return center + dir.Unit * (math.random() ^ (1/3)) * radius
	end,

	-- Random point on a sphere surface
	randomOnSphere = function(center, radius)
		local theta = math.random() * math.pi * 2
		local phi   = math.acos(math.random() * 2 - 1)
		return center + V(
			math.sin(phi) * math.cos(theta),
			math.sin(phi) * math.sin(theta),
			math.cos(phi)
		) * radius
	end,

	-- Random point in a circle (XZ)
	randomInCircle = function(center, radius)
		local angle = math.random() * math.pi * 2
		local r     = math.sqrt(math.random()) * radius
		return center + V(math.cos(angle) * r, 0, math.sin(angle) * r)
	end,

	-- Random point in a box
	randomInBox = function(center, size)
		return center + V(
			(math.random() - 0.5) * size.X,
			(math.random() - 0.5) * size.Y,
			(math.random() - 0.5) * size.Z
		)
	end,
}

-- ============================================================
-- 7. CAMERA & VIEW SPACE
-- ============================================================
Formulas.Camera = {
	-- Forward direction from a CFrame
	forward      = function(cf) return cf.LookVector end,

	-- Right direction from a CFrame
	right        = function(cf) return cf.RightVector end,

	-- Up direction from a CFrame
	up           = function(cf) return cf.UpVector end,

	-- Convert world vector to camera-local
	toLocal      = function(cf, worldVec) return cf:VectorToObjectSpace(worldVec) end,

	-- Convert camera-local vector to world
	toWorld      = function(cf, localVec) return cf:VectorToWorldSpace(localVec) end,

	-- Convert world position to camera space
	posToLocal   = function(cf, worldPos) return cf:PointToObjectSpace(worldPos) end,

	-- Convert camera-space position to world
	posToWorld   = function(cf, localPos) return cf:PointToWorldSpace(localPos) end,

	-- Camera-relative movement input (WASD to world direction)
	inputToWorld = function(cf, inputX, inputZ)
		return (cf.LookVector * -inputZ + cf.RightVector * inputX).Unit
	end,

	-- Is a point in front of the camera?
	isInFront    = function(cf, point, maxDist)
		local local_ = cf:PointToObjectSpace(point)
		return local_.Z < 0 and (not maxDist or local_.Magnitude <= maxDist)
	end,

	-- Is a point within the camera frustum? (approximate)
	inFrustum    = function(cf, point, fovDeg, aspect, near, far)
		local local_ = cf:PointToObjectSpace(point)
		if local_.Z > -near or local_.Z < -far then return false end
		local halfV = math.tan(math.rad(fovDeg * 0.5)) * -local_.Z
		local halfH = halfV * aspect
		return math.abs(local_.X) <= halfH and math.abs(local_.Y) <= halfV
	end,
}

-- ============================================================
-- 8. MOVEMENT & PHYSICS
-- ============================================================
Formulas.Movement = {
	-- Velocity needed to reach target in time
	velocityTo   = function(from, to, time)
		return (to - from) / time
	end,

	-- Displacement over time at velocity
	displacement = function(velocity, time) return velocity * time end,

	-- Projectile launch velocity (for target at distance d, height h)
	-- gravity: positive number (e.g., 196.2)
	-- Returns (velocity, time)
	projectile   = function(from, to, gravity, angleDeg)
		local diff = to - from
		local xz   = V(diff.X, 0, diff.Z).Magnitude
		local y    = diff.Y
		local angle = math.rad(angleDeg)
		local v2 = (gravity * xz * xz) / (2 * math.cos(angle)^2 * (xz * math.tan(angle) - y))
		if v2 < 0 then return nil, nil end
		local v = math.sqrt(v2)
		local dir = V(diff.X, 0, diff.Z).Unit
		local vel = dir * (v * math.cos(angle)) + V(0, v * math.sin(angle), 0)
		local t = xz / (v * math.cos(angle))
		return vel, t
	end,

	-- Optimal launch angle for max range (45 degrees in flat gravity)
	optimalAngle = function() return math.rad(45) end,

	-- Jump velocity needed to reach height h
	jumpVelocity = function(height, gravity)
		return math.sqrt(2 * gravity * height)
	end,

	-- Max height reached by a jump velocity
	jumpHeight   = function(velocity, gravity)
		return (velocity * velocity) / (2 * gravity)
	end,

	-- Air time for a jump
	airTime      = function(velocity, gravity)
		return 2 * velocity / gravity
	end,

	-- Terminal velocity with drag
	terminalVelocity = function(gravity, dragCoeff)
		return math.sqrt(gravity / dragCoeff)
	end,

	-- Apply drag to a velocity
	applyDrag    = function(velocity, drag, dt)
		return velocity * math.exp(-drag * dt)
	end,

	-- Air resistance (linear)
	airResist    = function(velocity, coeff, dt)
		return velocity * (1 - coeff * dt)
	end,

	-- Gravity acceleration per frame
	gravityStep  = function(velocity, gravity, dt)
		return velocity + V(0, -gravity * dt, 0)
	end,

	-- Bounce off a surface (with restitution)
	bounce       = function(velocity, normal, restitution)
		restitution = restitution or 1
		local normalComponent = normal * velocity:Dot(normal)
		local tangent = velocity - normalComponent
		return tangent - normalComponent * restitution
	end,

	-- Friction on a surface
	friction     = function(velocity, normal, coeff, dt)
		local tangent = Formulas.Direction.slide(velocity, normal)
		if tangent.Magnitude < 1e-6 then return V(0, 0, 0) end
		local decel = tangent.Unit * coeff * dt
		if decel.Magnitude >= tangent.Magnitude then return V(0, 0, 0) end
		return tangent - decel
	end,

	-- Centripetal acceleration (for circular motion)
	centripetal  = function(velocity, radius)
		return velocity.Magnitude^2 / radius
	end,

	-- Circular motion position
	circleMotion = function(center, radius, angle)
		return center + V(math.cos(angle) * radius, 0, math.sin(angle) * radius)
	end,

	-- Orbit position around a center
	orbit        = function(center, radius, angle, tilt)
		local x = math.cos(angle) * radius
		local z = math.sin(angle) * radius
		local y = math.sin(angle) * (tilt or 0) * radius
		return center + V(x, y, z)
	end,
}

-- ============================================================
-- 9. AI & PATHFINDING
-- ============================================================
Formulas.AI = {
	-- Seek: steer toward a target
	seek         = function(position, target, maxSpeed)
		return (target - position).Unit * maxSpeed
	end,

	-- Flee: steer away from a target
	flee         = function(position, target, maxSpeed)
		return (position - target).Unit * maxSpeed
	end,

	-- Arrive: slow down as you approach
	arrive       = function(position, target, maxSpeed, slowRadius)
		local toTarget = target - position
		local dist = toTarget.Magnitude
		if dist < 1e-6 then return V(0, 0, 0) end
		local speed = maxSpeed
		if dist < slowRadius then
			speed = maxSpeed * (dist / slowRadius)
		end
		return toTarget.Unit * speed
	end,

	-- Pursue: aim ahead of a moving target
	pursue       = function(position, targetPos, targetVel, maxSpeed)
		local toTarget = targetPos - position
		local lookAhead = toTarget.Magnitude / maxSpeed
		local futurePos = targetPos + targetVel * lookAhead
		return Formulas.AI.seek(position, futurePos, maxSpeed)
	end,

	-- Evade: run from where a target will be
	evade        = function(position, targetPos, targetVel, maxSpeed)
		local toTarget = position - targetPos
		local lookAhead = toTarget.Magnitude / maxSpeed
		local futurePos = targetPos + targetVel * lookAhead
		return Formulas.AI.flee(position, futurePos, maxSpeed)
	end,

	-- Wander: random steering on a circle ahead
	wander       = function(position, forward, wanderAngle, wanderRadius, wanderDist)
		local circleCenter = position + forward * wanderDist
		local offset = V(math.cos(wanderAngle) * wanderRadius, 0, math.sin(wanderAngle) * wanderRadius)
		return circleCenter + offset - position
	end,

	-- Separation: steer away from neighbors
	separation   = function(position, neighbors, desiredDist)
		local steer = V(0, 0, 0)
		for _, n in ipairs(neighbors) do
			local toMe = position - n
			local dist = toMe.Magnitude
			if dist < desiredDist and dist > 1e-6 then
				steer += toMe.Unit * (desiredDist - dist) / desiredDist
			end
		end
		return steer
	end,

	-- Alignment: match neighbor velocities
	alignment    = function(neighbors)
		if #neighbors == 0 then return V(0, 0, 0) end
		local sum = V(0, 0, 0)
		for _, v in ipairs(neighbors) do sum += v end
		return (sum / #neighbors).Unit
	end,

	-- Cohesion: steer toward neighbor centroid
	cohesion     = function(position, neighbors)
		if #neighbors == 0 then return V(0, 0, 0) end
		local sum = V(0, 0, 0)
		for _, p in ipairs(neighbors) do sum += p end
		local center = sum / #neighbors
		return (center - position).Unit
	end,

	-- Flocking: combine separation + alignment + cohesion
	flock        = function(position, velocity, neighbors, weights)
		weights = weights or { sep = 1.5, ali = 1.0, coh = 1.0 }
		local sep = Formulas.AI.separation(position, neighbors, 10) * weights.sep
		local ali = Formulas.AI.alignment(neighbors) * weights.ali
		local coh = Formulas.AI.cohesion(position, neighbors) * weights.coh
		return sep + ali + coh
	end,

	-- Field of view check
	inFOV        = function(eye, forward, target, fovDeg, maxDist)
		local toTarget = target - eye
		local dist = toTarget.Magnitude
		if dist > maxDist then return false end
		local angle = math.deg(math.acos(math.clamp(forward.Unit:Dot(toTarget.Unit), -1, 1)))
		return angle <= fovDeg * 0.5
	end,

	-- Line of sight (raycast direction)
	lineOfSight  = function(from, to) return (to - from).Unit end,
}

-- ============================================================
-- 10. PROCEDURAL & NOISE
-- ============================================================
Formulas.Procedural = {
	-- Hash a Vector3 into a pseudo-random number (0-1)
	hash         = function(v)
		local n = v.X * 374761393 + v.Y * 668265263 + v.Z * 2147483647
		n = bit32.bxor(n, bit32.rshift(n, 13))
		n = n * 1274126177
		return (bit32.band(n, 0xFFFFFF) / 0xFFFFFF)
	end,

	-- Value noise at integer lattice
	valueNoise   = function(v)
		local i = V(math.floor(v.X), math.floor(v.Y), math.floor(v.Z))
		local f = v - i
		f = V(f.X*f.X*(3-2*f.X), f.Y*f.Y*(3-2*f.Y), f.Z*f.Z*(3-2*f.Z))
		local function corner(dx, dy, dz)
			return Formulas.Procedural.hash(i + V(dx, dy, dz))
		end
		local c000 = corner(0,0,0); local c100 = corner(1,0,0)
		local c010 = corner(0,1,0); local c110 = corner(1,1,0)
		local c001 = corner(0,0,1); local c101 = corner(1,0,1)
		local c011 = corner(0,1,1); local c111 = corner(1,1,1)
		local x00 = c000 + (c100 - c000) * f.X
		local x10 = c010 + (c110 - c010) * f.X
		local x01 = c001 + (c101 - c001) * f.X
		local x11 = c011 + (c111 - c011) * f.X
		local y0 = x00 + (x10 - x00) * f.Y
		local y1 = x01 + (x11 - x01) * f.Y
		return y0 + (y1 - y0) * f.Z
	end,

	-- Fractal Brownian Motion (layered noise)
	fbm          = function(v, octaves, lacunarity, gain)
		octaves = octaves or 4
		lacunarity = lacunarity or 2.0
		gain = gain or 0.5
		local sum = 0
		local amp = 1
		local freq = 1
		local max = 0
		for _ = 1, octaves do
			sum += Formulas.Procedural.valueNoise(v * freq) * amp
			max += amp
			amp *= gain
			freq *= lacunarity
		end
		return sum / max
	end,

	-- Domain warp (distort coordinates before noise)
	warp         = function(v, strength)
		return v + V(
			Formulas.Procedural.valueNoise(v),
			Formulas.Procedural.valueNoise(v + V(5.2, 1.3, 7.1)),
			Formulas.Procedural.valueNoise(v + V(9.7, 3.4, 2.8))
		) * strength
	end,

	-- Distribute N points on a sphere (Fibonacci lattice)
	fibonacciSphere = function(n, radius)
		local points = {}
		local golden = math.pi * (3 - math.sqrt(5))
		for i = 0, n - 1 do
			local y = 1 - (i / (n - 1)) * 2
			local r = math.sqrt(1 - y * y)
			local theta = golden * i
			table.insert(points, V(math.cos(theta) * r, y, math.sin(theta) * r) * radius)
		end
		return points
	end,

	-- Scatter points in a volume with minimum spacing (Poisson-like)
	scatter      = function(count, minDist, bounds)
		local points = {}
		local attempts = 0
		while #points < count and attempts < count * 30 do
			attempts += 1
			local p = Formulas.Spatial.randomInBox(V(0,0,0), bounds)
			local ok = true
			for _, q in ipairs(points) do
				if (p - q).Magnitude < minDist then ok = false; break end
			end
			if ok then table.insert(points, p) end
		end
		return points
	end,
}

-- ============================================================
-- 11. COMBAT & TARGETING
-- ============================================================
Formulas.Combat = {
	-- Lead a moving target (predict intercept point)
	lead         = function(shooter, targetPos, targetVel, projSpeed)
		local toTarget = targetPos - shooter
		local a = targetVel:Dot(targetVel) - projSpeed * projSpeed
		local b = 2 * toTarget:Dot(targetVel)
		local c = toTarget:Dot(toTarget)
		local disc = b * b - 4 * a * c
		if disc < 0 then return targetPos end
		local t = (-b - math.sqrt(disc)) / (2 * a)
		if t < 0 then t = (-b + math.sqrt(disc)) / (2 * a) end
		return targetPos + targetVel * t
	end,

	-- Spread a shot direction by an angle cone
	spread       = function(dir, angleDeg)
		local angle = math.rad(angleDeg)
		local random = V(
			math.random() * 2 - 1,
			math.random() * 2 - 1,
			math.random() * 2 - 1
		)
		if random.Magnitude < 1e-6 then return dir end
		local perp = dir:Cross(random).Unit
		return Formulas.Direction.rotateAround(dir, perp, angle * math.random())
	end,

	-- Knockback direction from an explosion
	knockback    = function(explosionPos, targetPos, force)
		return (targetPos - explosionPos).Unit * force
	end,

	-- Cone check (is target in a cone?)
	inCone       = function(origin, forward, target, angleDeg, range)
		local toTarget = target - origin
		local dist = toTarget.Magnitude
		if dist > range then return false end
		local angle = math.deg(math.acos(math.clamp(forward.Unit:Dot(toTarget.Unit), -1, 1)))
		return angle <= angleDeg * 0.5
	end,

	-- Damage falloff by distance
	falloff      = function(distance, minDist, maxDist, maxDmg, minDmg)
		if distance <= minDist then return maxDmg end
		if distance >= maxDist then return minDmg end
		local t = (distance - minDist) / (maxDist - minDist)
		return maxDmg + (minDmg - maxDmg) * t
	end,

	-- Aim at a target with a turret (return yaw and pitch)
	aimAt        = function(turretPos, targetPos)
		local diff = targetPos - turretPos
		local yaw = math.atan2(-diff.X, -diff.Z)
		local pitch = math.asin(diff.Unit.Y)
		return yaw, pitch
	end,
}

-- ============================================================
-- 12. UTILITY & CONVERSION
-- ============================================================
Formulas.Utility = {
	-- Vector3 to CFrame (look in direction)
	toCFrame     = function(pos, lookDir, upDir)
		return CFrame.lookAt(pos, pos + lookDir, upDir or V(0, 1, 0))
	end,

	-- CFrame position to Vector3
	fromCFrame   = function(cf) return cf.Position end,

	-- Vector3 to table (for serialization)
	toTable      = function(v) return { X = v.X, Y = v.Y, Z = v.Z } end,

	-- Table to Vector3 (from serialization)
	fromTable    = function(t) return V(t.X, t.Y, t.Z) end,

	-- Format for debug printing
	format       = function(v) return string.format("(%.1f, %.1f, %.1f)", v.X, v.Y, v.Z) end,

	-- Check if two vectors are approximately equal
	approx       = function(a, b, epsilon)
		epsilon = epsilon or 1e-6
		return (a - b).Magnitude < epsilon
	end,

	-- Vector3 to HSV-like color (for debug visualization)
	toColor      = function(v)
		return Color3.new(
			math.clamp(math.abs(v.X) / 255, 0, 1),
			math.clamp(math.abs(v.Y) / 255, 0, 1),
			math.clamp(math.abs(v.Z) / 255, 0, 1)
		)
	end,

	-- Convert Vector3 direction to a compass heading (degrees, 0 = North)
	toCompass    = function(dir)
		local deg = math.deg(math.atan2(dir.X, -dir.Z))
		return (deg + 360) % 360
	end,

	-- Convert compass heading to Vector3 direction
	fromCompass  = function(deg)
		local rad = math.rad(deg)
		return V(math.sin(rad), 0, -math.cos(rad))
	end,
}

return Formulas