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