CCCPaste

BallPhysics Renderer

local module = {}

-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

-- Modules
local Modules = ReplicatedStorage:WaitForChild("Modules")
local Packet = require(Modules.PacketService)

-- Packet Listener
local BallRender = Packet("BallRender", Packet.NumberS8, Packet.Any, Packet.Any, Packet.Any)

-- Storage
module.Balls = {}
module.UpdateConnections = {}

-- Utility: Calculates position of projectile at time `t`
function module.CalculateProjectilePosition(P0, V, t, PullForce)
	return P0 + V * t + 0.5 * PullForce * t * t
end

-- Handles starting the rendering of a projectile
function module.HandleBallStart(BallId, BallModel, MotionData)
	local Direction = MotionData.Direction
	local Speed = MotionData.Speed
	local CurrentPos = MotionData.CurrentPos
	local PullForce = MotionData.PullForce
	local Duration = MotionData.Duration
	local Velocity = Direction * Speed
	local StartTime = MotionData.StartTime

	-- Create or reuse projectile visual
	local PropBall = module.Balls[BallId]
	if not PropBall then
		PropBall = BallModel:Clone()
		PropBall.Parent = workspace
		PropBall.Anchored = true
		PropBall.Position = CurrentPos
		PropBall.CanCollide = false
		PropBall.Material = Enum.Material.Neon
		module.Balls[BallId] = PropBall
	end

	-- Create update function for this projectile
	module.UpdateConnections[BallId] = function(dt)
		local elapsedTime = tick() - StartTime
		local timeFraction = elapsedTime / Duration
		local newPosition = module.CalculateProjectilePosition(CurrentPos, Velocity, timeFraction, PullForce)

		PropBall.Position = newPosition

		-- Optional: Ground/floor collision detection
		local rayParams = RaycastParams.new()
		rayParams.FilterType = Enum.RaycastFilterType.Exclude
		rayParams.FilterDescendantsInstances = { PropBall }

		local radius = math.max(PropBall.Size.X, PropBall.Size.Y, PropBall.Size.Z)
		local groundCheck = workspace:Raycast(newPosition, Vector3.new(0, -radius, 0), rayParams)
	end
end

-- Handles stopping the projectile motion
function module.HandleBallStop(BallId, LastPosition)
	if module.Balls[BallId] and module.UpdateConnections[BallId] then
		module.UpdateConnections[BallId] = nil
		module.Balls[BallId].Position = LastPosition
	end
end

-- Listener: Handle server events
module.StaticConnections = {
	Start = BallRender.OnClientEvent:Connect(function(RenderType, BallId, ...)
		local args = { ... }
		if RenderType == 0 then
			local BallModel = args[1]
			local MotionData = args[2]
			module.HandleBallStart(BallId, BallModel, MotionData)
		elseif RenderType == 1 then
			local LastPosition = args[1]
			module.HandleBallStop(BallId, LastPosition)
		end
	end)
}

-- Main Update Loop: Call every frame
function module.Update(dt)
	for BallId, UpdateFn in pairs(module.UpdateConnections) do
		task.spawn(UpdateFn, dt)
	end
end

return module