CCCPaste

GK Diving

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