local module = {}
module.__index = module
--// Services
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--// Assets
local GKAnimations = ReplicatedStorage:WaitForChild("Animations"):WaitForChild("GK")
--// Utility: Computes two axis-aligned positions forming a right angle from points A and B
local function computeAxisAlignedRightPoints(A, B)
local y = A.Y -- keep in the same horizontal plane
local Cx = Vector3.new(A.X, y, B.Z)
local Cz = Vector3.new(B.X, y, A.Z)
return Cx, Cz
end
--// Utility: Difference in height between two points
local function getHeightDifference(a: Vector3, b: Vector3): number
return b.Y - a.Y
end
--// Utility: Returns absolute vertical distance between two points
local function getAbsoluteHeightDifference(a: Vector3, b: Vector3): number
return math.abs(b.Y - a.Y)
end
--// Calculates urgency-based dive speed depending on ball's horizontal distance to goal
local function computeDiveSpeedFromGoal(ball, nearestGoalPoint, minSpeed, maxSpeed)
local ballPos = ball.Position
local ballSpeed = ball.Velocity.Magnitude * 0.5
-- Horizontal distance to goal in 3D space
local dist = math.sqrt((ballPos.X - nearestGoalPoint.X)^2 + (ballPos.Y - nearestGoalPoint.Y) + (ballPos.Z - nearestGoalPoint.Z)^2)
if dist == 0 then
return maxSpeed
end
local raw = ballSpeed / dist
return math.clamp(raw, minSpeed, maxSpeed)
end
--// Makes the goalkeeper dive in a ballistic arc from startPos to endPos
local function diveToPosition(gkModel, startPos, endPos, peakHeight, arrivalThreshold, speed, ballspeed)
speed = speed or 0.5
--// Get humanoid and root part
local humanoid = gkModel:FindFirstChildOfClass("Humanoid")
local rootPart = gkModel:FindFirstChild("HumanoidRootPart") or gkModel.PrimaryPart
if not humanoid or not rootPart then return end
--// Enter dive state
humanoid.PlatformStand = true
humanoid:ChangeState(Enum.HumanoidStateType.Freefall)
--// Ballistic arc physics setup
local g = workspace.Gravity or 196.2
local y0, y1 = startPos.Y, endPos.Y
local h = peakHeight
--// Ensure peak is above target
local minH = y1 - y0
if h < minH then
h = minH + 1
end
--// Compute vertical velocity and flight time
local v0y = math.sqrt(2 * g * h)
local tUp = v0y / g
local fallDistance = h + (y0 - y1)
local tDown = math.sqrt(2 * math.max(fallDistance, 0) / g)
local tTotal = tUp + tDown
--// Horizontal velocity
local displacement = endPos - startPos
local v0xz = Vector3.new(displacement.X, 0, displacement.Z) / tTotal
local initialVelocity = Vector3.new(
v0xz.X * speed,
v0y * speed,
v0xz.Z * speed
)
--// Apply impulse to dive
rootPart.AssemblyLinearVelocity = initialVelocity
--// Marker for dive state
local DV = Instance.new("StringValue", gkModel.PrimaryPart)
DV.Name = "DiveVelocity"
--// Stop when close enough to destination
local conn
conn = RunService.Heartbeat:Connect(function()
if (rootPart.Position - endPos).Magnitude <= arrivalThreshold then
conn:Disconnect()
DV:Destroy()
-- Stop horizontal movement, keep vertical momentum
local av = rootPart.AssemblyLinearVelocity
rootPart.AssemblyLinearVelocity = Vector3.new(0, av.Y, 0)
humanoid.PlatformStand = false
end
end)
end
--// Checks if goalkeeper collided with the ball while diving
local function CheckForBall(Character: Model, data)
local Ball: Part = data.Ball
while true do
if not Character.HumanoidRootPart then break end
if not Character.HumanoidRootPart:FindFirstChild("DiveVelocity") then break end
--// Create a temporary hitbox representing the goalkeeper's body
local Hitbox = Instance.new("Part")
Hitbox.CanCollide = false
Hitbox.Anchored = true
Hitbox.CFrame = Character:GetPivot()
Hitbox.Size = Character:GetExtentsSize()
Hitbox.Transparency = 1
Hitbox.Parent = workspace
--// Detect proximity to ball
local nearestPoint = Hitbox:GetClosestPointOnSurface(Ball.Position)
if (Ball.Position - nearestPoint).Magnitude < 3 then
Ball:Destroy()
Character.HumanoidRootPart.AssemblyLinearVelocity *= 0.25
break
end
Hitbox:Destroy()
task.wait(RunService.Heartbeat:Wait())
end
end
--// Default dive speed
local DiveSpeed = 0.75
--// Action handlers
local Actions = {
["DU"] = function(self, data, t, pos)
local Character: Model = self.Character
local FData = data.FieldData
local Ball = FData.Ball
local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
--// Stop any current animation
local PlayingAnimations = Humanoid:GetPlayingAnimationTracks()
if PlayingAnimations[1] then
PlayingAnimations[1]:Stop()
end
--// Play dive animation
local Track = Humanoid:LoadAnimation(GKAnimations.LDive)
Track:AdjustSpeed(2)
Track:Play()
Humanoid.Jump = true
--// Start ragdoll after animation nears end
task.spawn(function()
repeat task.wait() until Track.TimePosition > Track.Length * 0.85
local Ragdoll = require(script.RagdollModule)
Ragdoll:Ragdoll(Character)
Character.PrimaryPart.AssemblyAngularVelocity = Vector3.new(180, 0, 0)
end)
--// Predict where the ball is going to end up
local PredictedPosition
local nearestGoalPos
repeat
PredictedPosition = Ball.Position + Ball.AssemblyLinearVelocity * RunService.Heartbeat:Wait()
nearestGoalPos = FData.Zone.Goal:GetClosestPointOnSurface(Ball.Position)
task.wait()
until Character.PrimaryPart.Position.Y >= nearestGoalPos.Y or (PredictedPosition - nearestGoalPos).Magnitude < 3
--// Dynamically compute dive urgency
local speed = computeDiveSpeedFromGoal(Ball, nearestGoalPos, 0.5, 1.25)
--// Determine better final position via raycast
local BetterPos
local Params = RaycastParams.new()
Params.FilterType = Enum.RaycastFilterType.Include
Params.FilterDescendantsInstances = {FData.Zone.Goal}
local rc = workspace:Raycast(Ball.Position, Ball.AssemblyLinearVelocity)
if rc and rc.Instance then
BetterPos = rc.Position
end
--// Compute required dive height
local Height = getHeightDifference(Character.PrimaryPart.Position, BetterPos)
if Height < 0 then
Height = 0
end
--// Compute arc mid-point
local PosA = Character.PrimaryPart.Position
local PosC = BetterPos
local PosB = computeAxisAlignedRightPoints(PosA, PosC)
PosB = Vector3.new(PosB.X, PosC.Y, PosB.Z)
local O = (Vector2.new(PosA.X, PosA.Z) + Vector2.new(PosB.X, PosB.Z)) / 2
--// Optional: Visual debug
local PartA = Instance.new("Part", workspace)
PartA.Position = PosA
PartA.Size = Vector3.new(1, 1, 1)
PartA.Anchored = true
PartA.CanCollide = false
Instance.new("Highlight", PartA).FillColor = Color3.new(0.043, 0.09, 1)
local PartB = Instance.new("Part", workspace)
PartB.Position = PosB
PartB.Size = Vector3.new(1, 1, 1)
PartB.Anchored = true
PartB.CanCollide = false
Instance.new("Highlight", PartB).FillColor = Color3.new(0.118, 1, 0.22)
--// Perform dive and check for ball save
diveToPosition(Character, PosA, PosB, Height, 0.25, speed, Ball.AssemblyLinearVelocity.Magnitude * RunService.Heartbeat:Wait())
CheckForBall(Character, FData)
end,
}
--// Initializes the goalkeeper module with a character model
function module.Init(Model: Model)
local self = setmetatable({}, module)
self.Character = Model
return self
end
--// Triggers a specific goalie action
function module:DoAction(Action, PositionToHit, t)
if Actions[Action] then
Actions[Action](self, PositionToHit, t)
end
end
return module