Загрузка данных


local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- ================= НАСТРОЙКИ =================
local Settings = {
	HoverHeight = 0.5,   -- как в серверном скрипте
	GroundReach = 1.2,

	MaxSpeed = 42,
	MaxReverse = 8,
	Accel = 20,
	Brake = 45,
	Drag = 7,
	TurnRate = 2.4,
	Grip = 10,

	JumpPower = 34,
	AirSteer = 0.6,
	FlipTime = 0.75,

	-- Вили (езда на заднем колесе)
	WheelieKey = Enum.KeyCode.V,
	WheelieAngle = 0.55,    -- наклон носа вверх (радианы, 0.55 примерно 31°)
	WheelieMinSpeed = 6,    -- ниже этой скорости вили срывается
	WheelieSmooth = 6,

	DismountKey = Enum.KeyCode.C,
	BarspinKey = Enum.KeyCode.Q,
	TailwhipKey = Enum.KeyCode.E,
	BackflipKey = Enum.KeyCode.R,
	FrontflipKey = Enum.KeyCode.T,

	PoseAnimationId = "rbxassetid://89427909166247",
}
-- =============================================

local DARKCOL = Color3.fromRGB(28, 30, 38)
local ACCENT = Color3.fromRGB(230, 60, 60)

local player = Players.LocalPlayer
local trickEvent = ReplicatedStorage:WaitForChild("ScooterTrick")

local character, humanoid
local active = false
local seat, core, attach, align, poseTrack, stepConn
local ui, msgLabel, scoreLabel, wheelieBtn
local heading, speed = 0, 0
local jumpQueued = false
local jumpTimer = 0
local wasGrounded = true
local air = nil
local flipAngle, flipTarget = 0, 0
local score = 0
local msgToken = 0
local wheelieAmt, wheelieTime = 0, 0
local buttonHeld = false

-- ---------- интерфейс ----------
local function makeUI()
	local pg = player:WaitForChild("PlayerGui")
	local old = pg:FindFirstChild("ScooterUI")
	if old then old:Destroy() end

	ui = Instance.new("ScreenGui")
	ui.Name = "ScooterUI"
	ui.ResetOnSpawn = false
	ui.Parent = pg

	scoreLabel = Instance.new("TextLabel")
	scoreLabel.AnchorPoint = Vector2.new(1, 0)
	scoreLabel.Position = UDim2.new(1, -20, 0, 20)
	scoreLabel.Size = UDim2.fromOffset(220, 36)
	scoreLabel.BackgroundTransparency = 1
	scoreLabel.Font = Enum.Font.GothamBold
	scoreLabel.TextSize = 24
	scoreLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
	scoreLabel.TextStrokeTransparency = 0.5
	scoreLabel.TextXAlignment = Enum.TextXAlignment.Right
	scoreLabel.Text = "Очки: " .. score
	scoreLabel.Parent = ui

	msgLabel = Instance.new("TextLabel")
	msgLabel.AnchorPoint = Vector2.new(0.5, 1)
	msgLabel.Position = UDim2.new(0.5, 0, 1, -120)
	msgLabel.Size = UDim2.fromOffset(700, 44)
	msgLabel.BackgroundTransparency = 1
	msgLabel.Font = Enum.Font.GothamBold
	msgLabel.TextSize = 30
	msgLabel.TextStrokeTransparency = 0.4
	msgLabel.Text = ""
	msgLabel.Parent = ui

	local hint = Instance.new("TextLabel")
	hint.AnchorPoint = Vector2.new(0, 1)
	hint.Position = UDim2.new(0, 20, 1, -20)
	hint.Size = UDim2.fromOffset(520, 24)
	hint.BackgroundTransparency = 1
	hint.Font = Enum.Font.GothamMedium
	hint.TextSize = 15
	hint.TextColor3 = Color3.fromRGB(230, 230, 235)
	hint.TextStrokeTransparency = 0.6
	hint.TextXAlignment = Enum.TextXAlignment.Left
	hint.Text = "Q барспин · E тейлвип · R/T флипы · V вили · C слезть"
	hint.Parent = ui

	wheelieBtn = Instance.new("TextButton")
	wheelieBtn.AnchorPoint = Vector2.new(1, 1)
	wheelieBtn.Position = UDim2.new(1, -24, 1, -24)
	wheelieBtn.Size = UDim2.fromOffset(160, 54)
	wheelieBtn.BackgroundColor3 = DARKCOL
	wheelieBtn.BackgroundTransparency = 0.12
	wheelieBtn.AutoButtonColor = false
	wheelieBtn.Font = Enum.Font.GothamBold
	wheelieBtn.TextSize = 20
	wheelieBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
	wheelieBtn.Text = "ВИЛИ  [" .. Settings.WheelieKey.Name .. "]"
	wheelieBtn.Parent = ui

	local corner = Instance.new("UICorner")
	corner.CornerRadius = UDim.new(0, 14)
	corner.Parent = wheelieBtn
	local stroke = Instance.new("UIStroke")
	stroke.Color = ACCENT
	stroke.Thickness = 2
	stroke.Parent = wheelieBtn

	wheelieBtn.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			buttonHeld = true
		end
	end)
	wheelieBtn.InputEnded:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			buttonHeld = false
		end
	end)
end

local function showMessage(text, color)
	if not msgLabel then return end
	msgLabel.Text = text
	msgLabel.TextColor3 = color
	msgToken += 1
	local t = msgToken
	task.delay(2.2, function()
		if t == msgToken and msgLabel then msgLabel.Text = "" end
	end)
end

local function addScore(pts)
	score += pts
	if scoreLabel then scoreLabel.Text = "Очки: " .. score end
end

-- ---------- трюки ----------
local function addTrick(name, pts)
	table.insert(air.tricks, { name = name, pts = pts })
end

local function doTrick(name)
	if not air then return end
	if name == "Barspin" then
		if air.bar then return end
		air.bar = true
		trickEvent:FireServer("Barspin")
		addTrick("Барспин", 50)
	elseif name == "Tailwhip" then
		if air.whip then return end
		air.whip = true
		trickEvent:FireServer("Tailwhip")
		addTrick("Тейлвип", 100)
	elseif name == "Backflip" then
		if math.abs(flipTarget - flipAngle) > 0.1 then return end
		flipTarget += 2 * math.pi
		addTrick("Бэкфлип", 200)
	elseif name == "Frontflip" then
		if math.abs(flipTarget - flipAngle) > 0.1 then return end
		flipTarget -= 2 * math.pi
		addTrick("Фронтфлип", 200)
	end
end

local function endWheelie()
	if wheelieTime >= 1 then
		local pts = math.floor(wheelieTime) * 50
		addScore(pts)
		showMessage(string.format("Вили %.1f c  +%d", wheelieTime, pts), Color3.fromRGB(255, 210, 90))
	end
	wheelieTime = 0
end

local function handleLanding()
	local a = air
	air = nil
	local fa = flipAngle
	flipAngle, flipTarget = 0, 0
	if not a or a.time < 0.3 then return end

	local rem = (fa + math.pi) % (2 * math.pi) - math.pi
	if math.abs(rem) > 0.9 then
		speed *= 0.35
		showMessage("БЕЙЛ!", Color3.fromRGB(255, 80, 80))
		return
	end

	local halves = math.floor(math.abs(a.spin) / math.pi + 0.3)
	if halves >= 1 then
		table.insert(a.tricks, { name = (halves * 180) .. "°", pts = 100 * halves })
	end
	if #a.tricks == 0 then return end

	local sum, names = 0, {}
	for _, t in ipairs(a.tricks) do
		sum += t.pts
		table.insert(names, t.name)
	end
	local total = sum * #a.tricks
	addScore(total)
	local combo = #a.tricks > 1 and ("  (x" .. #a.tricks .. ")") or ""
	showMessage(table.concat(names, " + ") .. "  +" .. total .. combo, Color3.fromRGB(120, 255, 140))
end

-- ---------- начало и конец поездки ----------
local function stopRide()
	if not active then return end
	active = false
	ContextActionService:UnbindAction("ScooterJump")
	if stepConn then stepConn:Disconnect() stepConn = nil end
	if poseTrack then poseTrack:Stop(0.2) poseTrack = nil end
	if align then align:Destroy() align = nil end
	if attach then attach:Destroy() attach = nil end
	if ui then ui:Destroy() ui = nil end
	wheelieBtn = nil
	buttonHeld = false
	wheelieAmt, wheelieTime = 0, 0
	seat, core, air = nil, nil, nil
end

local function jumpAction(_, state)
	if state == Enum.UserInputState.Begin then
		jumpQueued = true
	end
	return Enum.ContextActionResult.Sink
end

local step

local function startRide(s)
	if active then return end
	local c = s.Parent and s.Parent:FindFirstChild("Core")
	if not c then return end
	active = true
	seat = s
	core = c

	local look = core.CFrame.LookVector
	heading = math.atan2(-look.X, -look.Z)
	speed, jumpTimer, wasGrounded, air = 0, 0, true, nil
	flipAngle, flipTarget = 0, 0
	wheelieAmt, wheelieTime = 0, 0
	jumpQueued = false

	attach = Instance.new("Attachment")
	attach.Name = "ScooterAttach"
	attach.Parent = core

	align = Instance.new("AlignOrientation")
	align.Mode = Enum.OrientationAlignmentMode.OneAttachment
	align.Attachment0 = attach
	align.RigidityEnabled = false
	align.MaxTorque = 1e9
	align.MaxAngularVelocity = math.huge
	align.Responsiveness = 60
	align.CFrame = core.CFrame.Rotation
	align.Parent = core

	local animator = humanoid:FindFirstChildOfClass("Animator")
	if animator and Settings.PoseAnimationId ~= "" then
		local a = Instance.new("Animation")
		a.AnimationId = Settings.PoseAnimationId
		poseTrack = animator:LoadAnimation(a)
		poseTrack.Priority = Enum.AnimationPriority.Action
		poseTrack.Looped = true
		poseTrack:Play(0.2)
	end

	ContextActionService:BindActionAtPriority(
		"ScooterJump", jumpAction, false,
		Enum.ContextActionPriority.High.Value + 10, Enum.KeyCode.Space
	)

	makeUI()
	stepConn = RunService.Heartbeat:Connect(step)
end

-- ---------- физика каждый кадр ----------
step = function(dt)
	if not active then return end
	if not core or not core.Parent or not humanoid or humanoid.Health <= 0 or humanoid.SeatPart ~= seat then
		stopRide()
		return
	end

	local pos = core.Position
	jumpTimer = math.max(jumpTimer - dt, 0)

	local typing = UserInputService:GetFocusedTextBox() ~= nil
	local throttle, steer = 0, 0
	local wheelieHeld = buttonHeld
	if not typing then
		if UserInputService:IsKeyDown(Enum.KeyCode.W) then throttle += 1 end
		if UserInputService:IsKeyDown(Enum.KeyCode.S) then throttle -= 1 end
		if UserInputService:IsKeyDown(Enum.KeyCode.D) then steer += 1 end
		if UserInputService:IsKeyDown(Enum.KeyCode.A) then steer -= 1 end
		if UserInputService:IsKeyDown(Settings.WheelieKey) then wheelieHeld = true end
	end

	local pwPrev = wheelieAmt * Settings.WheelieAngle
	local hoverPrev = Settings.HoverHeight + 1.55 * math.sin(pwPrev) + 0.15 * (math.cos(pwPrev) - 1)

	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Exclude
	params.FilterDescendantsInstances = { character, core.Parent }
	local ray = workspace:Raycast(pos, Vector3.new(0, -(hoverPrev + Settings.GroundReach), 0), params)
	local grounded = ray ~= nil and jumpTimer <= 0

	-- взлёт и приземление
	if grounded and not wasGrounded then
		handleLanding()
	elseif (not grounded) and wasGrounded then
		air = { time = 0, spin = 0, tricks = {}, bar = false, whip = false }
		flipAngle, flipTarget = 0, 0
	end
	wasGrounded = grounded
	if air then air.time += dt end

	-- поворот
	local speedFactor = math.clamp(math.abs(speed) / 8, 0, 1)
	local turn = Settings.TurnRate * (0.35 + 0.65 * speedFactor)
		* (1 - 0.4 * math.clamp(math.abs(speed) / Settings.MaxSpeed, 0, 1))
	local dh = -steer * turn * dt
	if grounded then
		heading += dh
	else
		heading += dh * Settings.AirSteer
		if air then air.spin += dh * Settings.AirSteer end
	end
	seat:SetAttribute("Heading", heading)

	local fwdFlat = Vector3.new(-math.sin(heading), 0, -math.cos(heading))
	local v = core.AssemblyLinearVelocity
	local targetCF

	if grounded then
		local n = ray.Normal
		local planeDir = fwdFlat - n * fwdFlat:Dot(n)
		planeDir = planeDir.Magnitude > 0.01 and planeDir.Unit or fwdFlat

		local actual = v:Dot(planeDir)
		if actual < speed - 6 then speed = math.max(actual, 0) end

		if throttle > 0 then
			speed = math.min(speed + Settings.Accel * dt, Settings.MaxSpeed)
		elseif throttle < 0 then
			if speed > 0 then
				speed = math.max(speed - Settings.Brake * dt, 0)
			else
				speed = math.max(speed - Settings.Accel * 0.5 * dt, -Settings.MaxReverse)
			end
		else
			if speed > 0 then
				speed = math.max(speed - Settings.Drag * dt, 0)
			else
				speed = math.min(speed + Settings.Drag * dt, 0)
			end
		end

		-- вили: нос вверх, самокат едет на заднем колесе
		local want = wheelieHeld and speed >= Settings.WheelieMinSpeed
		wheelieAmt += ((want and 1 or 0) - wheelieAmt) * math.clamp(dt * Settings.WheelieSmooth, 0, 1)
		local pw = wheelieAmt * Settings.WheelieAngle
		local hoverNow = Settings.HoverHeight + 1.55 * math.sin(pw) + 0.15 * (math.cos(pw) - 1)

		if pw > 0.3 then
			wheelieTime += dt
		elseif wheelieTime > 0 then
			endWheelie()
		end

		local dist = (pos.Y - ray.Position.Y) * n.Y
		local err = hoverNow - dist
		local vn = math.clamp(err * 15, -20, 25)
		local vt = (v - n * v:Dot(n)):Lerp(planeDir * speed, math.clamp(dt * Settings.Grip, 0, 1))
		core.AssemblyLinearVelocity = vt + n * vn

		targetCF = CFrame.fromMatrix(Vector3.zero, planeDir:Cross(n), n) * CFrame.Angles(pw, 0, 0)
	else
		wheelieAmt -= wheelieAmt * math.clamp(dt * 8, 0, 1)
		if wheelieTime > 0 then endWheelie() end

		if flipAngle ~= flipTarget then
			local rate = 2 * math.pi / Settings.FlipTime
			local diff = flipTarget - flipAngle
			flipAngle += math.sign(diff) * math.min(math.abs(diff), rate * dt)
		end
		targetCF = CFrame.Angles(0, heading, 0) * CFrame.Angles(flipAngle, 0, 0)
	end

	-- прыжок
	if jumpQueued then
		jumpQueued = false
		if grounded then
			jumpTimer = 0.2
			local cv = core.AssemblyLinearVelocity
			core.AssemblyLinearVelocity = Vector3.new(cv.X, Settings.JumpPower, cv.Z)
		end
	end

	align.CFrame = targetCF

	-- подсветка кнопки вили
	if wheelieBtn then
		wheelieBtn.BackgroundColor3 = DARKCOL:Lerp(ACCENT, math.clamp(wheelieAmt, 0, 1))
	end
end

-- ---------- клавиши ----------
UserInputService.InputBegan:Connect(function(input, processed)
	if not active or processed then return end
	local k = input.KeyCode
	if k == Settings.DismountKey then
		humanoid.Sit = false
		humanoid:UnequipTools()
	elseif k == Settings.BarspinKey then
		doTrick("Barspin")
	elseif k == Settings.TailwhipKey then
		doTrick("Tailwhip")
	elseif k == Settings.BackflipKey then
		doTrick("Backflip")
	elseif k == Settings.FrontflipKey then
		doTrick("Frontflip")
	end
end)

-- ---------- привязка к персонажу ----------
local function onSeatChanged()
	local s = humanoid and humanoid.SeatPart
	if s and s:GetAttribute("IsScooter") then
		startRide(s)
	else
		stopRide()
	end
end

local function setup(char)
	stopRide()
	character = char
	humanoid = char:WaitForChild("Humanoid")
	humanoid:GetPropertyChangedSignal("SeatPart"):Connect(onSeatChanged)
	onSeatChanged()
end

player.CharacterAdded:Connect(setup)
if player.Character then setup(player.Character) end