giga hub pasta
Key Required

giga hub pasta

(UPDATE) Drive A Kukirin!ghfhfhfhfhghfhfhfhfh 1.5K views 4K executesUpdated Oct 4, 2026
Unrated
00
Lua Script•2341 lines (77675 chars)
--[[
	GIGAMENU v2.0 — by zyru
	Кастомизация самоката Kukirin G4  (workspace["Kukirin G4"])
	Тип: LocalScript  ->  StarterPlayer > StarterPlayerScripts

	  F           вилли вкл/выкл  (синхронно с переключателем Auto Wheelie [F])
	  Shift       заморозить / разморозить самокат (пока сидишь на нём)
	  RightShift  показать / спрятать меню
	  "—"         свернуть меню в полоску,  "X" — скрыть

	Страницы:
	  Presets        готовые пресеты
	  Customization  вилли, фриз, вилли-бар (ADD), трейл, Rainbow Kukirin G4, цвета
	  Underglow      подсветка снизу (без видимого блока — только свет)
	  Test           список деталей самоката: клик = подсветка на самокате + смена цвета
	  Test2          PUBLIC / PRIVATE пресеты и Cloud key (поделиться темой с другом)

	Если что-то красится не то — смотри Output (F9): скрипт сам печатает,
	как разложил детали по группам. Имена можно вписать в PART_NAMES.
]]

local Players      = game:GetService("Players")
local UIS          = game:GetService("UserInputService")
local RS           = game:GetService("RunService")
local Workspace    = game:GetService("Workspace")
local TweenService = game:GetService("TweenService")
local HttpService  = game:GetService("HttpService")

local player    = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")

do
	local old = playerGui:FindFirstChild("GIGAMENU")
	if old then old:Destroy() end
end

local RGB = Color3.fromRGB
local WHITE, BLACK = Color3.new(1, 1, 1), Color3.new(0, 0, 0)

---------------------------------------------------------------
-- НАСТРОЙКИ
---------------------------------------------------------------
local CONFIG = {
	SCOOTER_NAME = "Kukirin G4",

	WHEELIE_KEY  = Enum.KeyCode.F,
	FREEZE_KEY   = Enum.KeyCode.J,
	MENU_KEY     = Enum.KeyCode.RightShift,
	WHEELIE_HOLD = false,  -- true = вилли пока держишь клавишу, false = вкл/выкл по нажатию

	WHEELIE_ANGLE = 65,    -- градусов
	WHEELIE_POWER = 6,
	WHEELIE_RAMP  = 80,    -- град/сек

	-- Куда у PrimaryPart смотрит "нос" (локальные координаты).
	-- Если вместо вилли самокат задирает ЗАД — Vector3.new(0, 0, 1).
	FORWARD_LOCAL = Vector3.new(0, 0, -1),

	RIDE_RADIUS = 8,         -- если нет сиденья/сварки: считаем что ты "на самокате", когда ближе стадов

	UNDERGLOW_HEIGHT = 0.08, -- высота источника света (доля высоты модели)
	GLOW_BRIGHTNESS  = 4,
	GLOW_RANGE       = 20,

	TRAIL_WIDTH    = nil,  -- nil = подобрать по колесу (в студах)
	TRAIL_LIFETIME = 1.2,

	STRETCH_VALUE = 0.75,  -- для пресета "Resolution 4:3"

	WHEELIE_BAR_COLLIDE = false, -- true = вилли-бар физически касается земли

	RAINBOW_SPEED  = 0.25, -- скорость радуги
	RAINBOW_SPREAD = 0.6,  -- насколько радуга "растянута" вдоль самоката (0 = весь одним цветом)
	SWEEP_INTERVAL = 0.1,  -- как часто перепроверять цвета (сек) — защита от игры, которая их сбрасывает

	STRIP_TEXTURES        = true, -- убирать SurfaceAppearance/TextureID при покраске (иначе цвет не виден)
	UNCLASSIFIED_TO_FRAME = true, -- если Frame не найден — красить неопознанные детали как раму
}

local PART_NAMES = {
	Frame      = {"Frame", "Body", "Deck"},
	Wheel      = {"FrontWheel", "BackWheel"},
	Handlebar  = {"Handlebar", "Bars", "Steering"},
	Suspension = {"Suspension", "Shock"},
	WheelieBar = {"WheelieBar"},
}

local GROUP_ORDER = {"WheelieBar", "Wheel", "Suspension", "Handlebar", "Frame"}
local GROUP_SET = {}
for _, g in ipairs(GROUP_ORDER) do GROUP_SET[g] = true end

local KEYWORDS = {
	WheelieBar = {"wheelie"},
	Wheel      = {"wheel", "tire", "tyre", "rim"},
	Suspension = {"suspension", "shock", "spring", "fork"},
	Handlebar  = {"handlebar", "handle", "steer", "grip"},
	Frame      = {"frame", "body", "deck", "chassis", "stem"},
}

---------------------------------------------------------------
-- СОСТОЯНИЕ
---------------------------------------------------------------
local state = {
	colors      = {},              -- [группа] = Color3 (nil = заводской)
	materials   = {},              -- [группа] = Enum.Material
	partColors  = {},              -- [путь детали] = Color3 (страница Test)
	underglow   = true,
	glowColor   = RGB(0, 90, 255),
	trail       = false,
	trailColor  = RGB(0, 255, 255),
	smokeColor  = nil,             -- nil = как в игре
	wheelieBar  = false,           -- нажали ADD
}

local settings = {stretch = false}
local rainbow  = {}                -- [цель] = true  (All / Trail / Underglow / Smoke / группа)
local wheelie  = {on = false, target = 0}
local freeze   = {on = false, saved = {}}
local riding   = false

local conns = {}
local function connect(signal, fn)
	local c = signal:Connect(fn)
	table.insert(conns, c)
	return c
end

-- то, что определяется ниже (GUI), но нужно раньше
local notify, updateStatus, refreshTestList, closeTestPanel
local wheelieToggleUI, freezeToggleUI, wbButtonUpdate
local testVisible = false

local function getHumanoid()
	local c = player.Character
	return c and c:FindFirstChildOfClass("Humanoid")
end

---------------------------------------------------------------
-- ДВИЖОК ПОКРАСКИ
-- Цвет считается из state (деталь > группа), и принудительно
-- возвращается, если игра его сбросила (сразу по сигналу + раз в 0.1с).
---------------------------------------------------------------
local originals     = setmetatable({}, {__mode = "k"})
local partKeyOf     = setmetatable({}, {__mode = "k"})
local partGroupOf   = setmetatable({}, {__mode = "k"})
local phaseOf       = setmetatable({}, {__mode = "k"})
local hooked        = setmetatable({}, {__mode = "k"})

local groups = {}
for _, g in ipairs(GROUP_ORDER) do groups[g] = {} end
local unclassified, allParts, partsByKey, keyList = {}, {}, {}, {}
local scooter, rootPart, dirty, descConn = nil, nil, true, nil
local scanVersion = 0

local rainbowT, rainbowColor = 0, Color3.fromHSV(0, 1, 1)

local function stripTexture(p, force)
	if not CONFIG.STRIP_TEXTURES then return end
	local o = originals[p]
	if not o then return end
	if o.stripped and not force then return end
	o.stripped = true
	for _, ch in ipairs(p:GetChildren()) do
		if ch:IsA("SurfaceAppearance") then
			o.sa = o.sa or {}
			table.insert(o.sa, ch)
			ch.Parent = nil
		end
	end
	if p:IsA("MeshPart") and p.TextureID ~= "" then
		o.tex = p.TextureID
		pcall(function() p.TextureID = "" end)
	end
end

local function restoreTexture(p)
	local o = originals[p]
	if not o or not o.stripped then return end
	o.stripped = false
	if o.sa then
		for _, sa in ipairs(o.sa) do
			pcall(function() sa.Parent = p end)
		end
		o.sa = nil
	end
	if o.tex then
		local t = o.tex
		pcall(function() p.TextureID = t end)
		o.tex = nil
	end
end

local function sameColor(a, b)
	return math.abs(a.R - b.R) < 0.002 and math.abs(a.G - b.G) < 0.002 and math.abs(a.B - b.B) < 0.002
end

local function desiredColor(p)
	if rainbow.All then
		return Color3.fromHSV((rainbowT + (phaseOf[p] or 0)) % 1, 1, 1)
	end
	local pk = partKeyOf[p]
	local pc = pk and state.partColors[pk]
	if pc then return pc end
	local g = partGroupOf[p]
	if g then
		if rainbow[g] then return rainbowColor end
		return state.colors[g]
	end
	return nil
end

local function desiredMaterial(p)
	local g = partGroupOf[p]
	return g and state.materials[g] or nil
end

local function enforcePart(p)
	if not p.Parent then return end
	local want = desiredColor(p)
	if want then
		local o = originals[p]
		if o and not o.stripped then stripTexture(p) end
		if p:IsA("UnionOperation") and not p.UsePartColor then p.UsePartColor = true end
		if not sameColor(p.Color, want) then p.Color = want end
	end
	local m = desiredMaterial(p)
	if m and p.Material ~= m then p.Material = m end
end

local function hasPaint()
	return next(state.colors) ~= nil or next(state.materials) ~= nil
		or next(state.partColors) ~= nil or next(rainbow) ~= nil
end

local function enforceAll()
	if not hasPaint() then return end
	for _, p in ipairs(allParts) do enforcePart(p) end
end

-- вернуть всё в заводское и накатить текущее состояние заново
local function repaintAll()
	for _, p in ipairs(allParts) do
		local o = originals[p]
		if o and p.Parent then
			restoreTexture(p)
			if not sameColor(p.Color, o.Color) then p.Color = o.Color end
			if p.Material ~= o.Material then p.Material = o.Material end
			if o.UsePartColor ~= nil and p:IsA("UnionOperation") then p.UsePartColor = o.UsePartColor end
		end
	end
	enforceAll()
end

local function hookPart(p)
	if hooked[p] then return end
	hooked[p] = true
	local function recheck()
		if p.Parent and not rainbow.All then enforcePart(p) end
	end
	p:GetPropertyChangedSignal("Color"):Connect(recheck)
	p:GetPropertyChangedSignal("Material"):Connect(recheck)
	p.ChildAdded:Connect(function(ch)
		if ch:IsA("SurfaceAppearance") and desiredColor(p) then
			stripTexture(p, true)
		end
	end)
	if p:IsA("MeshPart") then
		p:GetPropertyChangedSignal("TextureID"):Connect(function()
			if desiredColor(p) and p.TextureID ~= "" then stripTexture(p, true) end
		end)
	end
end

---------------------------------------------------------------
-- САМОКАТ: поиск и группы деталей
---------------------------------------------------------------
local lastSearch = -100

local function isFx(obj)
	return obj.Name:sub(1, 5) == "GIGA_" or obj:FindFirstAncestor("GIGA_FX") ~= nil
end

local function classify(part)
	for _, g in ipairs(GROUP_ORDER) do
		for _, n in ipairs(PART_NAMES[g]) do
			if part.Name == n then return g end
		end
	end
	local node = part
	while node and node ~= scooter do
		local ln = node.Name:lower()
		for _, g in ipairs(GROUP_ORDER) do
			for _, kw in ipairs(KEYWORDS[g]) do
				if ln:find(kw, 1, true) then return g end
			end
		end
		node = node.Parent
	end
	return nil
end

local function relPath(p)
	local names = {}
	local node = p
	while node and node ~= scooter do
		table.insert(names, 1, node.Name)
		node = node.Parent
	end
	if #names == 0 then return p.Name end
	return table.concat(names, "/")
end

local function normName(n)
	return (n:lower():gsub("[^%w]", ""))
end

local function searchScooter()
	local now = os.clock()
	if now - lastSearch < 2 then return nil end
	lastSearch = now
	local s = Workspace:FindFirstChild(CONFIG.SCOOTER_NAME, true)
	if s then return s end
	local needle = normName(CONFIG.SCOOTER_NAME)
	for _, d in ipairs(Workspace:GetDescendants()) do
		if d:IsA("Model") or d:IsA("BasePart") then
			local n = normName(d.Name)
			if n == needle or n:find("kukirin", 1, true) then return d end
		end
	end
	return nil
end

local function pickNearest(list)
	if #list <= 1 then return list[1] end
	local char = player.Character
	local hrp = char and char:FindFirstChild("HumanoidRootPart")
	if not hrp then return list[1] end
	local best, bd = nil, math.huge
	for _, s in ipairs(list) do
		local pos = s:IsA("Model") and s:GetPivot().Position or s.Position
		local d = (pos - hrp.Position).Magnitude
		if d < bd then best, bd = s, d end
	end
	return best
end

local function directCandidates()
	local list = {}
	for _, c in ipairs(Workspace:GetChildren()) do
		if c.Name == CONFIG.SCOOTER_NAME and (c:IsA("Model") or c:IsA("BasePart")) then
			table.insert(list, c)
		end
	end
	return list
end

local function getScooter()
	local s = scooter
	-- если ты сидишь в самокате — это и есть наш самокат
	local hum = getHumanoid()
	local seat = hum and hum.SeatPart
	local ridden = seat and seat:FindFirstAncestor(CONFIG.SCOOTER_NAME)
	if ridden and ridden ~= scooter and (ridden:IsA("Model") or ridden:IsA("BasePart")) then
		s = ridden
	elseif not (s and s:IsDescendantOf(Workspace)) then
		s = pickNearest(directCandidates()) or searchScooter()
	end
	if s ~= scooter then
		scooter = s
		dirty = true
		if descConn then descConn:Disconnect() descConn = nil end
		if s then
			descConn = s.DescendantAdded:Connect(function() dirty = true end)
		end
	end
	return scooter
end

local function rescan()
	for _, g in ipairs(GROUP_ORDER) do groups[g] = {} end
	unclassified, allParts, partsByKey, keyList = {}, {}, {}, {}
	rootPart = nil
	scanVersion += 1
	local s = scooter
	if not s then
		if updateStatus then updateStatus() end
		return
	end
	local biggest, biggestVol = nil, 0
	local list = s:GetDescendants()
	if s:IsA("BasePart") then table.insert(list, s) end
	for _, obj in ipairs(list) do
		if obj:IsA("BasePart") and not isFx(obj) then
			if not originals[obj] then
				local entry = {Color = obj.Color, Material = obj.Material, Transparency = obj.Transparency}
				if obj:IsA("UnionOperation") then entry.UsePartColor = obj.UsePartColor end
				originals[obj] = entry
			end
			hookPart(obj)
			local g = classify(obj)
			partGroupOf[obj] = g
			if g then table.insert(groups[g], obj) else table.insert(unclassified, obj) end
			table.insert(allParts, obj)
			local key = relPath(obj)
			partKeyOf[obj] = key
			local bucket = partsByKey[key]
			if not bucket then
				bucket = {}
				partsByKey[key] = bucket
				table.insert(keyList, key)
			end
			table.insert(bucket, obj)
			local vol = obj.Size.X * obj.Size.Y * obj.Size.Z
			if vol > biggestVol then biggest, biggestVol = obj, vol end
		end
	end
	table.sort(keyList)
	if s:IsA("Model") and s.PrimaryPart then
		rootPart = s.PrimaryPart
	else
		rootPart = biggest
	end
	if CONFIG.UNCLASSIFIED_TO_FRAME and #groups.Frame == 0 then
		for _, p in ipairs(unclassified) do
			table.insert(groups.Frame, p)
			partGroupOf[p] = "Frame"
		end
	end

	-- фаза радуги: позиция детали вдоль самоката
	if rootPart then
		local f = CONFIG.FORWARD_LOCAL
		local dist, lo, hi = {}, math.huge, -math.huge
		for _, p in ipairs(allParts) do
			local v = rootPart.CFrame:PointToObjectSpace(p.Position):Dot(f)
			dist[p] = v
			if v < lo then lo = v end
			if v > hi then hi = v end
		end
		local span = math.max(hi - lo, 0.001)
		for p, v in pairs(dist) do
			phaseOf[p] = (v - lo) / span * CONFIG.RAINBOW_SPREAD
		end
	end

	-- новые/пересозданные детали красим сразу же
	enforceAll()
	if updateStatus then updateStatus() end
end

local function ensure()
	getScooter()
	if dirty then
		dirty = false
		rescan()
	end
end

local function getRoot()
	ensure()
	return rootPart
end

local function getFxFolder()
	local s = getScooter()
	if not s then return nil end
	local f = s:FindFirstChild("GIGA_FX")
	if not f then
		f = Instance.new("Folder")
		f.Name = "GIGA_FX"
		f.Parent = s
	end
	return f
end

local function cleanupOld()
	local s = scooter
	if not s then return end
	for _, d in ipairs(s:GetDescendants()) do
		if d.Name:sub(1, 5) == "GIGA_" then d:Destroy() end
	end
end

local function getRearWheel(fwd)
	local rear, best = nil, math.huge
	for _, w in ipairs(groups.Wheel) do
		local d = w.Position:Dot(fwd)
		if d < best then best, rear = d, w end
	end
	return rear
end

local function debugPrint()
	ensure()
	print("[GIGAMENU] scooter:", scooter and scooter:GetFullName() or "НЕ НАЙДЕН (проверь CONFIG.SCOOTER_NAME)")
	print("[GIGAMENU] root part:", rootPart and rootPart:GetFullName() or "nil")
	for _, g in ipairs(GROUP_ORDER) do
		local names = {}
		for _, p in ipairs(groups[g]) do table.insert(names, p.Name) end
		print(string.format("[GIGAMENU] %-10s (%d): %s", g, #names, table.concat(names, ", ")))
	end
	local un = {}
	for _, p in ipairs(unclassified) do table.insert(un, p.Name) end
	print(string.format("[GIGAMENU] без группы (%d): %s", #un, table.concat(un, ", ")))
end

-- следим за появлением нового самоката (респавн / клон при посадке) — ищем без задержки
connect(Workspace.DescendantAdded, function(d)
	if d.Name == CONFIG.SCOOTER_NAME and (d:IsA("Model") or d:IsA("BasePart")) then
		lastSearch = -100
		dirty = true
	end
end)

local function groupDisplayColor(g)
	if state.colors[g] then return state.colors[g] end
	ensure()
	local p = groups[g][1]
	local o = p and originals[p]
	return o and o.Color or RGB(255, 255, 255)
end

---------------------------------------------------------------
-- "НА САМОКАТЕ?" и ЗАМОРОЗКА (Shift)
---------------------------------------------------------------
local function isRiding()
	local s = scooter
	local char = player.Character
	local hrp = char and char:FindFirstChild("HumanoidRootPart")
	if not (s and char and hrp) then return false end
	local hum = char:FindFirstChildOfClass("Humanoid")
	local seat = hum and hum.SeatPart
	if seat and seat:IsDescendantOf(s) then return true end
	local root = rootPart
	if not root then return false end
	local ok, parts = pcall(function() return root:GetConnectedParts(true) end)
	if ok then
		for _, p in ipairs(parts) do
			if p:IsDescendantOf(char) then return true end
		end
	end
	return (hrp.Position - root.Position).Magnitude <= CONFIG.RIDE_RADIUS
end

local function resetFreezeState()
	freeze.on = false
	freeze.saved = {}
	if freezeToggleUI then freezeToggleUI.set(false) end
end

-- возвращает true, если состояние стало таким, как просили
local function setFreeze(on)
	if on == freeze.on then return true end
	if on then
		ensure()
		if not rootPart or not isRiding() then return false end
		freeze.saved = {}
		for _, p in ipairs(allParts) do
			if p.Parent then
				freeze.saved[p] = p.Anchored
				p.AssemblyLinearVelocity = Vector3.zero
				p.AssemblyAngularVelocity = Vector3.zero
				p.Anchored = true
			end
		end
		freeze.on = true
	else
		for p, was in pairs(freeze.saved) do
			if p.Parent then p.Anchored = was end
		end
		freeze.saved = {}
		freeze.on = false
	end
	if freezeToggleUI then freezeToggleUI.set(freeze.on) end
	return true
end

local function checkFreeze()
	local hum = getHumanoid()
	local char = player.Character
	local hrp = char and char:FindFirstChild("HumanoidRootPart")
	if not hum or hum.Health <= 0 or not hrp or not rootPart
		or (hrp.Position - rootPart.Position).Magnitude > 40 then
		setFreeze(false)
	end
end

---------------------------------------------------------------
-- ВИЛЛИ-БАР (кнопка ADD): две распорки назад от заднего колеса + перекладина
---------------------------------------------------------------
local function newBarPart(folder, root, localCF, size)
	local p = Instance.new("Part")
	p.Name = "WheelieBar"
	p.Size = size
	p.Material = Enum.Material.Metal
	p.Color = RGB(28, 28, 30)
	p.TopSurface = Enum.SurfaceType.Smooth
	p.BottomSurface = Enum.SurfaceType.Smooth
	p.CanCollide = CONFIG.WHEELIE_BAR_COLLIDE
	p.CanQuery = false
	p.CanTouch = false
	p.Massless = true
	p.CFrame = root.CFrame * localCF
	p.Parent = folder
	local w = Instance.new("WeldConstraint")
	w.Part0 = root
	w.Part1 = p
	w.Parent = p
	return p
end

local function buildWheelieBar()
	ensure()
	local root, s = rootPart, scooter
	if not root or not s then return false end
	local old = s:FindFirstChild("WheelieBarAdded")
	if old then old:Destroy() end

	local fwd = root.CFrame:VectorToWorldSpace(CONFIG.FORWARD_LOCAL)
	local rear = getRearWheel(fwd)
	local back = -CONFIG.FORWARD_LOCAL.Unit
	local side = CONFIG.FORWARD_LOCAL:Cross(Vector3.yAxis).Unit
	local center, r, halfW
	if rear then
		center = root.CFrame:PointToObjectSpace(rear.Position)
		r = math.max(rear.Size.X, rear.Size.Y, rear.Size.Z) / 2
		halfW = math.min(rear.Size.X, rear.Size.Y, rear.Size.Z) / 2 + 0.2
	else
		center = back * (math.max(root.Size.X, root.Size.Z) / 2) + Vector3.new(0, -root.Size.Y / 2 + 0.4, 0)
		r, halfW = 0.4, 0.4
	end

	local t = math.clamp(r * 0.18, 0.06, 0.2)
	local a = center + back * (r * 0.4) + Vector3.new(0, r * 0.35, 0)
	local b = center + back * (r * 1.9) + Vector3.new(0, -r * 0.6, 0)

	local folder = Instance.new("Folder")
	folder.Name = "WheelieBarAdded"
	folder.Parent = s

	for _, sgn in ipairs({-1, 1}) do
		local A = a + side * (sgn * halfW)
		local B = b + side * (sgn * halfW)
		local len = (B - A).Magnitude
		newBarPart(folder, root, CFrame.lookAt((A + B) / 2, B), Vector3.new(t, t, len))
	end
	newBarPart(folder, root, CFrame.lookAt(b, b + side), Vector3.new(t * 1.4, t * 1.4, halfW * 2 + t))

	dirty = true -- пересканируем: новые детали попадут в группу WheelieBar и сразу покрасятся
	return true
end

---------------------------------------------------------------
-- АНДЕРГЛОУ: только свет (невидимый источник, приварен к самокату)
---------------------------------------------------------------
local glow = {}

local function destroyGlow()
	if glow.part then glow.part:Destroy() end
	glow = {}
end

local function setGlowColorRaw(c)
	if glow.point   then glow.point.Color = c end
	if glow.surface then glow.surface.Color = c end
end

local function localBounds(root, model)
	local minV = Vector3.new(math.huge, math.huge, math.huge)
	local maxV = Vector3.new(-math.huge, -math.huge, -math.huge)
	local list = model:GetDescendants()
	if model:IsA("BasePart") then table.insert(list, model) end
	for _, p in ipairs(list) do
		if p:IsA("BasePart") and not isFx(p) then
			local rel = root.CFrame:ToObjectSpace(p.CFrame)
			local h = p.Size / 2
			for _, sx in ipairs({-1, 1}) do
				for _, sy in ipairs({-1, 1}) do
					for _, sz in ipairs({-1, 1}) do
						local c = rel * Vector3.new(h.X * sx, h.Y * sy, h.Z * sz)
						minV = Vector3.new(math.min(minV.X, c.X), math.min(minV.Y, c.Y), math.min(minV.Z, c.Z))
						maxV = Vector3.new(math.max(maxV.X, c.X), math.max(maxV.Y, c.Y), math.max(maxV.Z, c.Z))
					end
				end
			end
		end
	end
	return minV, maxV
end

local function buildGlow()
	destroyGlow()
	if not state.underglow then return end
	local root = getRoot()
	local folder = getFxFolder()
	if not root or not folder then return end

	local minV, maxV = localBounds(root, scooter)
	local size = maxV - minV
	local center = (minV + maxV) / 2
	local y = minV.Y + math.max(size.Y * CONFIG.UNDERGLOW_HEIGHT, 0.12)

	local emitter = Instance.new("Part")
	emitter.Name = "GIGA_Underglow"
	emitter.Size = Vector3.new(0.2, 0.2, 0.2)
	emitter.Transparency = 1          -- блока не видно, светят только лампы
	emitter.CanCollide = false
	emitter.CanQuery = false
	emitter.CanTouch = false
	emitter.CastShadow = false
	emitter.Massless = true
	emitter.Material = Enum.Material.SmoothPlastic
	emitter.CFrame = root.CFrame * CFrame.new(center.X, y, center.Z)
	emitter.Parent = folder

	local weld = Instance.new("WeldConstraint")
	weld.Part0 = root
	weld.Part1 = emitter
	weld.Parent = emitter

	local c = state.glowColor
	local point = Instance.new("PointLight")
	point.Color = c
	point.Brightness = CONFIG.GLOW_BRIGHTNESS
	point.Range = CONFIG.GLOW_RANGE
	point.Shadows = false
	point.Parent = emitter

	local surf = Instance.new("SurfaceLight")
	surf.Face = Enum.NormalId.Bottom
	surf.Angle = 170
	surf.Color = c
	surf.Brightness = CONFIG.GLOW_BRIGHTNESS
	surf.Range = CONFIG.GLOW_RANGE
	surf.Shadows = false
	surf.Parent = emitter

	glow = {part = emitter, point = point, surface = surf, root = root}
end

---------------------------------------------------------------
-- ТРЕЙЛ: плоская полоса света по земле за задним колесом
---------------------------------------------------------------
local trail = {}

local function destroyTrail()
	for _, o in pairs(trail) do
		if typeof(o) == "Instance" then o:Destroy() end
	end
	trail = {}
end

local function buildTrail()
	destroyTrail()
	if not state.trail then return end
	local root = getRoot()
	if not root then return end

	local fwd = root.CFrame:VectorToWorldSpace(CONFIG.FORWARD_LOCAL)
	local rear = getRearWheel(fwd)
	local localPos, r, width
	if rear then
		localPos = root.CFrame:PointToObjectSpace(rear.Position)
		r = math.max(rear.Size.X, rear.Size.Y, rear.Size.Z) / 2
		width = math.clamp(math.min(rear.Size.X, rear.Size.Y, rear.Size.Z) * 2.5, 0.8, 3)
	else
		localPos = -CONFIG.FORWARD_LOCAL * (math.max(root.Size.X, root.Size.Z) / 2) + Vector3.new(0, -root.Size.Y / 2, 0)
		r = 0.3
		width = 1
	end
	width = CONFIG.TRAIL_WIDTH or width

	-- поперёк самоката (локальная "правая" ось) -> лента лежит плоско на земле
	local side = CONFIG.FORWARD_LOCAL:Cross(Vector3.yAxis).Unit
	local ground = localPos + Vector3.new(0, -r + 0.06, 0)

	local a0 = Instance.new("Attachment")
	a0.Name = "GIGA_TrailA0"
	a0.Position = ground - side * (width / 2)
	a0.Parent = root
	local a1 = Instance.new("Attachment")
	a1.Name = "GIGA_TrailA1"
	a1.Position = ground + side * (width / 2)
	a1.Parent = root

	local t = Instance.new("Trail")
	t.Name = "GIGA_Trail"
	t.Attachment0 = a0
	t.Attachment1 = a1
	t.Color = ColorSequence.new(state.trailColor)
	t.Transparency = NumberSequence.new({NumberSequenceKeypoint.new(0, 0.05), NumberSequenceKeypoint.new(1, 1)})
	t.LightEmission = 1
	t.Lifetime = CONFIG.TRAIL_LIFETIME
	t.MinLength = 0.05
	t.FaceCamera = false
	t.Parent = root

	trail = {obj = t, a0 = a0, a1 = a1}
end

---------------------------------------------------------------
-- ДЫМ (Burnout Smoke)
---------------------------------------------------------------
local smoke = {list = {}}
local smokeOrig = setmetatable({}, {__mode = "k"})

local function setSmokeColor(c)
	for _, e in ipairs(smoke.list) do e.Color = ColorSequence.new(c) end
	if smoke.own then smoke.own.Color = ColorSequence.new(c) end
end

local function restoreSmoke()
	for _, e in ipairs(smoke.list) do
		if smokeOrig[e] then e.Color = smokeOrig[e] end
	end
	if smoke.own then smoke.own.Color = ColorSequence.new(RGB(230, 230, 230)) end
end

local function destroySmoke()
	if smoke.own then smoke.own:Destroy() end
	if smoke.att then smoke.att:Destroy() end
	smoke = {list = {}}
end

local function buildSmoke()
	destroySmoke()
	local s = getScooter()
	local root = getRoot()
	if not s or not root then return end

	-- ищем дым, который уже есть в модели игры
	local list = {}
	for _, d in ipairs(s:GetDescendants()) do
		if d:IsA("ParticleEmitter") and not isFx(d) then
			local n = (d.Name .. " " .. (d.Parent and d.Parent.Name or "")):lower()
			if n:find("smoke", 1, true) or n:find("burnout", 1, true) then
				if not smokeOrig[d] then smokeOrig[d] = d.Color end
				table.insert(list, d)
			end
		end
	end
	smoke.list = list

	-- нет своего дыма в модели — делаем дым у заднего колеса (идёт пока включено вилли)
	if #list == 0 then
		local fwd = root.CFrame:VectorToWorldSpace(CONFIG.FORWARD_LOCAL)
		local rear = getRearWheel(fwd)
		local pos
		if rear then
			local r = math.max(rear.Size.X, rear.Size.Y, rear.Size.Z) / 2
			pos = root.CFrame:PointToObjectSpace(rear.Position) + Vector3.new(0, -r * 0.6, 0)
		else
			pos = -CONFIG.FORWARD_LOCAL * (math.max(root.Size.X, root.Size.Z) / 2) + Vector3.new(0, -root.Size.Y / 2, 0)
		end
		local att = Instance.new("Attachment")
		att.Name = "GIGA_SmokeAtt"
		att.CFrame = CFrame.lookAt(pos, pos - CONFIG.FORWARD_LOCAL)
		att.Parent = root

		local e = Instance.new("ParticleEmitter")
		e.Name = "GIGA_Smoke"
		e.Texture = "rbxasset://textures/particles/smoke_main.dds"
		e.Color = ColorSequence.new(RGB(230, 230, 230))
		e.Rate = 45
		e.Lifetime = NumberRange.new(0.7, 1.3)
		e.Speed = NumberRange.new(4, 8)
		e.SpreadAngle = Vector2.new(20, 20)
		e.Rotation = NumberRange.new(0, 360)
		e.RotSpeed = NumberRange.new(-60, 60)
		e.Size = NumberSequence.new({NumberSequenceKeypoint.new(0, 1), NumberSequenceKeypoint.new(1, 4)})
		e.Transparency = NumberSequence.new({NumberSequenceKeypoint.new(0, 0.45), NumberSequenceKeypoint.new(1, 1)})
		e.EmissionDirection = Enum.NormalId.Front
		e.Enabled = wheelie.on
		e.Parent = att
		smoke.own = e
		smoke.att = att
	end

	if state.smokeColor then setSmokeColor(state.smokeColor) end
end

---------------------------------------------------------------
-- RAINBOW (All = весь Kukirin, либо по каждой цели отдельно)
---------------------------------------------------------------
local function savedColor(target)
	if target == "Underglow" then return state.glowColor end
	if target == "Trail" then return state.trailColor end
	if target == "Smoke" then return state.smokeColor end
	return state.colors[target]
end

local function setRainbow(target, on)
	rainbow[target] = on or nil
	if on then return end
	if target == "Underglow" then
		setGlowColorRaw(state.glowColor)
	elseif target == "Trail" then
		if trail.obj then trail.obj.Color = ColorSequence.new(state.trailColor) end
	elseif target == "Smoke" then
		if state.smokeColor then setSmokeColor(state.smokeColor) else restoreSmoke() end
	else
		repaintAll() -- All или группа деталей
	end
end

local function stopAllRainbow()
	local had = next(rainbow) ~= nil
	for k in pairs(rainbow) do rainbow[k] = nil end
	if had then
		setGlowColorRaw(state.glowColor)
		if trail.obj then trail.obj.Color = ColorSequence.new(state.trailColor) end
		if state.smokeColor then setSmokeColor(state.smokeColor) else restoreSmoke() end
	end
end

---------------------------------------------------------------
-- ПОДСВЕТКА ДЕТАЛИ НА САМОКАТЕ (страница Test)
---------------------------------------------------------------
local hlList = {}

local function clearHighlight()
	for _, h in ipairs(hlList) do h:Destroy() end
	hlList = {}
end

local function highlightKey(key)
	clearHighlight()
	local list = partsByKey[key]
	local folder = getFxFolder()
	if not list or not folder then return end
	for i, p in ipairs(list) do
		if i > 30 then break end
		local h = Instance.new("Highlight")
		h.Name = "GIGA_Highlight"
		h.Adornee = p
		h.FillColor = RGB(0, 220, 255)
		h.OutlineColor = WHITE
		h.FillTransparency = 0.8
		h.OutlineTransparency = 0
		h.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop
		h.Parent = folder
		table.insert(hlList, h)
	end
end

---------------------------------------------------------------
-- ГЛАВНЫЙ ЦИКЛ КАДРА: радуга, быстрая проверка цветов, пульс подсветки
---------------------------------------------------------------
local sweepAcc = 0
connect(RS.RenderStepped, function(dt)
	rainbowT = os.clock() * CONFIG.RAINBOW_SPEED
	rainbowColor = Color3.fromHSV(rainbowT % 1, 1, 1)

	-- что-то добавилось/пересоздалось в самокате — пересканируем и красим ДО отрисовки кадра
	if dirty then ensure() end

	local needParts = rainbow.All
	if not needParts then
		for k in pairs(rainbow) do
			if GROUP_SET[k] then needParts = true break end
		end
	end
	sweepAcc += dt
	if needParts or sweepAcc >= CONFIG.SWEEP_INTERVAL then
		sweepAcc = 0
		enforceAll()
	end

	if rainbow.Underglow then setGlowColorRaw(rainbowColor) end
	if rainbow.Trail and trail.obj then
		trail.obj.Color = ColorSequence.new(rainbowColor, Color3.fromHSV((rainbowT - 0.15) % 1, 1, 1))
	end
	if rainbow.Smoke then setSmokeColor(rainbowColor) end

	if #hlList > 0 then
		local k = math.sin(os.clock() * 6) * 0.5 + 0.5
		for _, h in ipairs(hlList) do
			h.FillTransparency = 0.7 + 0.3 * k
			h.OutlineTransparency = 0.3 * (1 - k)
		end
	end
end)

---------------------------------------------------------------
-- ВИЛЛИ (F)
---------------------------------------------------------------
local function setWheelie(on)
	wheelie.on = on
	if wheelieToggleUI then wheelieToggleUI.set(on) end
	if smoke.own then smoke.own.Enabled = on end
end

local function stepWheelie(dt)
	local goal = wheelie.on and CONFIG.WHEELIE_ANGLE or 0
	local maxStep = CONFIG.WHEELIE_RAMP * dt
	wheelie.target = wheelie.target + math.clamp(goal - wheelie.target, -maxStep, maxStep)
	if not wheelie.on and wheelie.target < 0.05 then
		wheelie.target = 0
		return
	end

	local root = getRoot()
	if not root or root.Anchored then return end

	local fwd = root.CFrame:VectorToWorldSpace(CONFIG.FORWARD_LOCAL).Unit
	local right = fwd:Cross(Vector3.yAxis)
	if right.Magnitude < 0.1 then return end
	right = right.Unit

	local pitch = math.deg(math.asin(math.clamp(fwd.Y, -1, 1)))
	local err = wheelie.target - pitch

	local rate = root.AssemblyAngularVelocity:Dot(right)
	local want = math.clamp(math.rad(err) * CONFIG.WHEELIE_POWER, -4, 4)
	local newRate = rate + (want - rate) * math.clamp(dt * 12, 0, 1)
	local dW = right * (newRate - rate)

	root.AssemblyAngularVelocity = root.AssemblyAngularVelocity + dW

	-- вращаем вокруг оси заднего колеса, чтобы оно не вдавливалось в землю
	local rear = getRearWheel(fwd)
	if rear then
		root.AssemblyLinearVelocity = root.AssemblyLinearVelocity
			+ dW:Cross(root.AssemblyCenterOfMass - rear.Position)
	end
end

---------------------------------------------------------------
-- ПРИМЕНИТЬ ВСЁ + watchdog (респавн самоката, посадка/слезание)
---------------------------------------------------------------
local printedDebug = false

local function applyAll()
	ensure()
	enforceAll()
	buildGlow()
	buildTrail()
	buildSmoke()
	if state.wheelieBar and scooter and not scooter:FindFirstChild("WheelieBarAdded") then
		buildWheelieBar()
	end
	local total = 0
	for _, g in ipairs(GROUP_ORDER) do total = total + #groups[g] end
	if total == 0 then
		warn("[GIGAMENU] детали самоката не распознаны — смотри вывод ниже и впиши имена в PART_NAMES")
	end
	if not printedDebug then
		printedDebug = true
		debugPrint()
	end
end

local appliedTo, wdAcc = nil, 1
connect(RS.Heartbeat, function(dt)
	stepWheelie(dt)

	wdAcc += dt
	if wdAcc < 0.25 then return end
	wdAcc = 0

	ensure()
	local s = scooter
	riding = (s ~= nil) and isRiding()

	if s ~= appliedTo then
		if freeze.on then resetFreezeState() end
		appliedTo = s
		if s then
			cleanupOld()
			dirty = true
			ensure()
			applyAll()
		end
	elseif s then
		if state.underglow and (not glow.part or not glow.part.Parent or glow.root ~= rootPart) then buildGlow() end
		if state.trail and (not trail.obj or not trail.obj.Parent) then buildTrail() end
		if state.wheelieBar and not s:FindFirstChild("WheelieBarAdded") then buildWheelieBar() end
		if freeze.on then checkFreeze() end
	end

	if updateStatus then updateStatus() end
	if testVisible and refreshTestList then refreshTestList(false) end
end)

-- посадка / слезание: сразу пересканируем самокат
do
	local function watchCharacter(char)
		local hum = char:WaitForChild("Humanoid", 10)
		if not hum then return end
		hum:GetPropertyChangedSignal("SeatPart"):Connect(function()
			dirty = true
			wdAcc = 1
		end)
	end
	if player.Character then task.spawn(watchCharacter, player.Character) end
	connect(player.CharacterAdded, function(char) task.spawn(watchCharacter, char) end)
end

---------------------------------------------------------------
-- СОХРАНЕНИЕ ТЕМ: данные <-> Cloud key
---------------------------------------------------------------
local ui = {refreshers = {}}
local function syncUI()
	for _, fn in ipairs(ui.refreshers) do fn() end
end

local hex, encodeKey, decodeKey, captureData, applyData
do
function hex(c)
	return string.format("%02x%02x%02x", math.floor(c.R * 255 + 0.5), math.floor(c.G * 255 + 0.5), math.floor(c.B * 255 + 0.5))
end

local function unhex(s)
	if type(s) ~= "string" or #s ~= 6 then return nil end
	local r, g, b = tonumber(s:sub(1, 2), 16), tonumber(s:sub(3, 4), 16), tonumber(s:sub(5, 6), 16)
	if r and g and b then return RGB(r, g, b) end
	return nil
end

local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
local B64IDX = {}
for i = 1, #B64 do B64IDX[B64:byte(i)] = i - 1 end

local function b64enc(s)
	local out, n = {}, 0
	for i = 1, #s, 3 do
		local a, b, c = s:byte(i, i + 2)
		local v = a * 65536 + (b or 0) * 256 + (c or 0)
		local c1 = bit32.rshift(v, 18) % 64
		local c2 = bit32.rshift(v, 12) % 64
		local c3 = bit32.rshift(v, 6) % 64
		local c4 = v % 64
		local piece = B64:sub(c1 + 1, c1 + 1) .. B64:sub(c2 + 1, c2 + 1)
		if b then piece = piece .. B64:sub(c3 + 1, c3 + 1) end
		if c then piece = piece .. B64:sub(c4 + 1, c4 + 1) end
		n += 1
		out[n] = piece
	end
	return table.concat(out)
end

local function b64dec(s)
	local out = {}
	for i = 1, #s, 4 do
		local chunk = s:sub(i, i + 3)
		local cnt = #chunk
		if cnt == 1 then return nil end
		local v = 0
		for j = 1, 4 do
			local ch = chunk:byte(j)
			local x = ch and B64IDX[ch]
			if ch and not x then return nil end
			v = v * 64 + (x or 0)
		end
		table.insert(out, string.char(bit32.rshift(v, 16) % 256))
		if cnt >= 3 then table.insert(out, string.char(bit32.rshift(v, 8) % 256)) end
		if cnt == 4 then table.insert(out, string.char(v % 256)) end
	end
	return table.concat(out)
end

local KEY_PREFIX = "GM1"

local function checksum(s)
	local h = 7
	for i = 1, #s do h = (h * 31 + s:byte(i)) % 65521 end
	return string.format("%04x", h)
end

function encodeKey(d)
	local ok, json = pcall(function() return HttpService:JSONEncode(d) end)
	if not ok then return nil end
	local payload = b64enc(json)
	return KEY_PREFIX .. "." .. payload .. "." .. checksum(payload)
end

function decodeKey(key)
	key = (tostring(key or ""):gsub("%s+", ""))
	if key == "" then return nil, "empty" end
	if #key > 40000 then return nil, "too long" end
	local prefix, payload, sum = key:match("^(%w+)%.([%w%-_]+)%.(%x+)$")
	if prefix ~= KEY_PREFIX then return nil, "wrong format" end
	if checksum(payload) ~= sum then return nil, "key is damaged" end
	local json = b64dec(payload)
	if not json then return nil, "bad data" end
	local ok, d = pcall(function() return HttpService:JSONDecode(json) end)
	if not ok or type(d) ~= "table" then return nil, "bad data" end
	return d
end

-- снимок текущей темы
function captureData(name)
	local d = {v = 1, n = name, c = {}, m = {}, p = {}}
	for g, c in pairs(state.colors) do d.c[g] = hex(c) end
	for g, m in pairs(state.materials) do d.m[g] = m.Name end
	for k, c in pairs(state.partColors) do d.p[k] = hex(c) end
	d.g, d.go = hex(state.glowColor), state.underglow and 1 or 0
	d.t, d.to = hex(state.trailColor), state.trail and 1 or 0
	if state.smokeColor then d.s = hex(state.smokeColor) end
	if rainbow.All then d.r = 1 end
	return d
end

-- применить тему из данных (всё остальное сбрасывается в заводское)
function applyData(d)
	for k in pairs(rainbow) do rainbow[k] = nil end
	state.colors, state.materials, state.partColors = {}, {}, {}

	if type(d.c) == "table" then
		for g, h in pairs(d.c) do
			local c = unhex(h)
			if c and GROUP_SET[g] then state.colors[g] = c end
		end
	end
	if type(d.m) == "table" then
		for g, n in pairs(d.m) do
			local ok, m = pcall(function() return Enum.Material[n] end)
			if ok and m and GROUP_SET[g] then state.materials[g] = m end
		end
	end
	if type(d.p) == "table" then
		for k, h in pairs(d.p) do
			local c = unhex(h)
			if c and type(k) == "string" then state.partColors[k] = c end
		end
	end

	local gc = unhex(d.g)
	if gc then state.glowColor = gc end
	if d.go ~= nil then state.underglow = (d.go == 1) end
	local tc = unhex(d.t)
	if tc then state.trailColor = tc end
	if d.to ~= nil then state.trail = (d.to == 1) end
	state.smokeColor = unhex(d.s)
	if d.r == 1 then rainbow.All = true end

	repaintAll()
	buildGlow()
	buildTrail()
	if state.smokeColor then setSmokeColor(state.smokeColor) else restoreSmoke() end
	syncUI()
end

end -- serialization

---------------------------------------------------------------
-- ПРЕСЕТЫ
---------------------------------------------------------------
local N, M, S = Enum.Material.Neon, Enum.Material.Metal, Enum.Material.SmoothPlastic

-- классические пресеты (красят только раму/колёса, андерглоу — если указан)
local function classic(name, frame, wheel, glowColor)
	return {name = name, kind = "classic", colors = {Frame = frame, Wheel = wheel, WheelieBar = frame}, glow = glowColor}
end

local READY_PRESETS = {
	{name = "Full Black", kind = "classic", colors = {Frame = BLACK, Wheel = BLACK, Handlebar = BLACK, Suspension = BLACK, WheelieBar = BLACK}},
	classic("Black + White", BLACK, WHITE, nil),
	classic("Black + Blue",  BLACK, RGB(0, 90, 255), nil),
	classic("White + Red",   WHITE, RGB(255, 0, 0), nil),
	classic("Black + Purp",  BLACK, RGB(140, 0, 255), nil),
	classic("White + Pink + Underglow", WHITE, RGB(255, 120, 200), RGB(255, 120, 200)),
	classic("Black + Cyan + Underglow", BLACK, RGB(0, 255, 255), RGB(0, 255, 255)),
	{name = "Resolution 4:3", kind = "resolution"},
}

local function classicToData(p)
	local d = {v = 1, n = p.name, c = {}, m = {}, p = {}}
	for g, c in pairs(p.colors) do d.c[g] = hex(c) end
	if p.glow then d.g, d.go = hex(p.glow), 1 end
	return d
end

local function setStretch(on)
	settings.stretch = on
	pcall(function() RS:UnbindFromRenderStep("GIGA_Stretch") end)
	if on then
		RS:BindToRenderStep("GIGA_Stretch", Enum.RenderPriority.Camera.Value + 1, function()
			local cam = Workspace.CurrentCamera
			if cam then
				cam.CFrame = cam.CFrame * CFrame.new(0, 0, 0, 1, 0, 0, 0, CONFIG.STRETCH_VALUE, 0, 0, 0, 1)
			end
		end)
	end
end

local function applyPreset(p)
	if p.kind == "resolution" then
		setStretch(not settings.stretch)
		if notify then notify("Resolution 4:3 " .. (settings.stretch and "ON" or "OFF")) end
		return
	end

	stopAllRainbow()
	state.materials = {}
	state.partColors = {}
	for g, c in pairs(p.colors) do state.colors[g] = c end
	if p.glow then
		state.underglow = true
		state.glowColor = p.glow
	end
	repaintAll()
	if p.glow then buildGlow() end
	syncUI()
end

---------------------------------------------------------------
-- GUI: палитра и хелперы (стиль как в видео)
---------------------------------------------------------------
local C = {
	bg     = RGB(18, 22, 28),
	row    = RGB(24, 28, 37),
	rowHi  = RGB(30, 35, 45),
	btn    = RGB(34, 40, 50),
	tabOff = RGB(14, 18, 22),
	tabOn  = RGB(26, 31, 40),
	text   = RGB(235, 238, 245),
	dim    = RGB(110, 118, 130),
	head   = RGB(74, 128, 150),
	accent = RGB(60, 124, 152),
	red    = RGB(226, 84, 80),
	track  = RGB(30, 35, 44),
}

local function new(class, props, parent)
	local o = Instance.new(class)
	for k, v in pairs(props) do o[k] = v end
	if parent then o.Parent = parent end
	return o
end

local function corner(inst, r)
	local c = Instance.new("UICorner")
	c.CornerRadius = UDim.new(0, r or 6)
	c.Parent = inst
	return c
end

local counters = setmetatable({}, {__mode = "k"})
local function bump(parent)
	counters[parent] = (counters[parent] or 0) + 1
	return counters[parent]
end

local function trim(s)
	return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
end

local gui = new("ScreenGui", {
	Name = "GIGAMENU", ResetOnSpawn = false, DisplayOrder = 50,
	ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
}, playerGui)

-- всплывающая подсказка снизу
local toast = new("TextLabel", {
	AnchorPoint = Vector2.new(0.5, 1), Position = UDim2.new(0.5, 0, 1, -28), Size = UDim2.fromOffset(0, 28),
	AutomaticSize = Enum.AutomaticSize.X, BackgroundColor3 = C.bg, BackgroundTransparency = 0.1,
	Text = "", TextColor3 = C.text, Font = Enum.Font.GothamMedium, TextSize = 12, Visible = false, ZIndex = 10,
	BorderSizePixel = 0,
}, gui)
corner(toast, 8)
new("UIPadding", {PaddingLeft = UDim.new(0, 14), PaddingRight = UDim.new(0, 14)}, toast)
local toastToken = 0
notify = function(text)
	toastToken += 1
	local my = toastToken
	toast.Text = text
	toast.Visible = true
	task.delay(2.2, function()
		if my == toastToken then toast.Visible = false end
	end)
end

local function makeDraggable(frame, handle)
	local dragging, dragStart, startPos = false, nil, nil
	handle.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			dragging = true
			dragStart = input.Position
			startPos = frame.Position
		end
	end)
	connect(UIS.InputChanged, function(input)
		if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
			local d = input.Position - dragStart
			frame.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + d.X, startPos.Y.Scale, startPos.Y.Offset + d.Y)
		end
	end)
	connect(UIS.InputEnded, function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			dragging = false
		end
	end)
end

-- слайдер 0..255 (как R/G/B в видео)
local function makeSlider(parent, position, size, initial, fillColor, onChange, scrollFrame)
	local hit = new("Frame", {Position = position, Size = size, BackgroundTransparency = 1, Active = true}, parent)
	local bar = new("Frame", {
		AnchorPoint = Vector2.new(0, 0.5), Position = UDim2.new(0, 0, 0.5, 0), Size = UDim2.new(1, 0, 0, 6),
		BackgroundColor3 = C.track, BorderSizePixel = 0,
	}, hit)
	corner(bar, 3)
	local fill = new("Frame", {Size = UDim2.new(0, 0, 1, 0), BackgroundColor3 = fillColor, BorderSizePixel = 0}, bar)
	corner(fill, 3)
	local knob = new("Frame", {
		AnchorPoint = Vector2.new(0.5, 0.5), Size = UDim2.fromOffset(12, 12), Position = UDim2.new(0, 0, 0.5, 0),
		BackgroundColor3 = WHITE, BorderSizePixel = 0, ZIndex = 2,
	}, bar)
	corner(knob, 6)

	local function visual(v)
		local a = math.clamp(v / 255, 0, 1)
		fill.Size = UDim2.new(a, 0, 1, 0)
		knob.Position = UDim2.new(a, 0, 0.5, 0)
	end
	visual(initial)

	local dragging = false
	local function update(x)
		local a = math.clamp((x - bar.AbsolutePosition.X) / math.max(bar.AbsoluteSize.X, 1), 0, 1)
		local v = math.floor(a * 255 + 0.5)
		visual(v)
		onChange(v)
	end
	local function lock(l)
		if scrollFrame then scrollFrame.ScrollingEnabled = not l end
	end

	hit.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			dragging = true
			lock(true)
			update(input.Position.X)
		end
	end)
	connect(UIS.InputChanged, function(input)
		if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
			update(input.Position.X)
		end
	end)
	connect(UIS.InputEnded, function(input)
		if dragging and (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
			dragging = false
			lock(false)
		end
	end)

	return {set = function(v) visual(v) end}
end

-- три слайдера R/G/B внутри panel; onChange вызывается только когда двигает пользователь
local CHANS = {{"R", RGB(240, 80, 84)}, {"G", RGB(80, 250, 81)}, {"B", RGB(83, 80, 238)}}
local function makeRGBSliders(panel, page, onChange, startOrder)
	local draft = {255, 255, 255}
	local sliders, labels = {}, {}
	for i, ch in ipairs(CHANS) do
		local line = new("Frame", {Size = UDim2.new(1, 0, 0, 18), BackgroundTransparency = 1, LayoutOrder = startOrder + i - 1}, panel)
		new("TextLabel", {
			Size = UDim2.fromOffset(14, 18), BackgroundTransparency = 1, Text = ch[1], TextColor3 = ch[2],
			Font = Enum.Font.GothamBold, TextSize = 10,
		}, line)
		labels[i] = new("TextLabel", {
			AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 0), Size = UDim2.fromOffset(30, 18),
			BackgroundTransparency = 1, Text = "255", TextColor3 = C.dim, Font = Enum.Font.Gotham, TextSize = 10,
			TextXAlignment = Enum.TextXAlignment.Right,
		}, line)
		sliders[i] = makeSlider(line, UDim2.fromOffset(20, 0), UDim2.new(1, -56, 1, 0), 255, ch[2], function(v)
			draft[i] = v
			labels[i].Text = tostring(v)
			onChange(RGB(draft[1], draft[2], draft[3]))
		end, page)
	end
	return {
		get = function() return RGB(draft[1], draft[2], draft[3]) end,
		set = function(c)
			draft = {math.floor(c.R * 255 + 0.5), math.floor(c.G * 255 + 0.5), math.floor(c.B * 255 + 0.5)}
			for i = 1, 3 do
				sliders[i].set(draft[i])
				labels[i].Text = tostring(draft[i])
			end
		end,
	}
end

local function textBox(parent, props)
	local tb = new("TextBox", {
		BackgroundColor3 = C.btn, TextColor3 = C.text, PlaceholderColor3 = C.dim, Font = Enum.Font.Gotham,
		TextSize = 11, TextXAlignment = Enum.TextXAlignment.Left, ClearTextOnFocus = false, BorderSizePixel = 0,
		ClipsDescendants = true, Text = "",
	}, nil)
	for k, v in pairs(props) do tb[k] = v end
	corner(tb, 6)
	new("UIPadding", {PaddingLeft = UDim.new(0, 8), PaddingRight = UDim.new(0, 8)}, tb)
	tb.Parent = parent
	return tb
end

---------------------------------------------------------------
-- GUI: окно
---------------------------------------------------------------
local W, H, HEAD_H = 480, 330, 46

local main = new("Frame", {
	Name = "Main", Size = UDim2.fromOffset(W, H), Position = UDim2.new(0.5, -W / 2, 0.5, -H / 2),
	BackgroundColor3 = C.bg, BackgroundTransparency = 0.08, BorderSizePixel = 0,
	ClipsDescendants = true, Active = true,
}, gui)
corner(main, 10)
new("UIStroke", {Color = WHITE, Transparency = 0.92, Thickness = 1}, main)

local sidebar, content, setMenuVisible
do
local topBar = new("Frame", {Name = "TopBar", Size = UDim2.new(1, 0, 0, HEAD_H), BackgroundTransparency = 1, Active = true}, main)
makeDraggable(main, topBar)

local dot = new("Frame", {Position = UDim2.fromOffset(12, 18), Size = UDim2.fromOffset(8, 8), BackgroundColor3 = C.accent, BorderSizePixel = 0}, topBar)
corner(dot, 4)
new("TextLabel", {
	Position = UDim2.fromOffset(28, 6), Size = UDim2.fromOffset(86, 18), BackgroundTransparency = 1,
	Text = "GIGAMENU", TextColor3 = C.text, Font = Enum.Font.GothamBold, TextSize = 14,
	TextXAlignment = Enum.TextXAlignment.Left,
}, topBar)
new("TextLabel", {
	Position = UDim2.fromOffset(28, 23), Size = UDim2.fromOffset(80, 14), BackgroundTransparency = 1,
	Text = "by матвейбадяг", TextColor3 = C.dim, Font = Enum.Font.Gotham, TextSize = 10,
	TextXAlignment = Enum.TextXAlignment.Left,
}, topBar)
local badge = new("TextLabel", {
	Position = UDim2.fromOffset(114, 12), Size = UDim2.fromOffset(36, 15), BackgroundColor3 = C.accent,
	Text = "v2.0", TextColor3 = WHITE, Font = Enum.Font.GothamBold, TextSize = 9,
}, topBar)
corner(badge, 4)

local minBtn = new("TextButton", {
	Position = UDim2.new(1, -58, 0, 11), Size = UDim2.fromOffset(24, 24), BackgroundTransparency = 1,
	Text = "—", TextColor3 = C.dim, Font = Enum.Font.GothamBold, TextSize = 13,
}, topBar)
local closeBtn = new("TextButton", {
	Position = UDim2.new(1, -30, 0, 14), Size = UDim2.fromOffset(18, 18), BackgroundColor3 = RGB(70, 100, 114),
	Text = "X", TextColor3 = WHITE, Font = Enum.Font.GothamBold, TextSize = 10, AutoButtonColor = true,
}, topBar)
corner(closeBtn, 9)

sidebar = new("Frame", {Position = UDim2.fromOffset(10, 50), Size = UDim2.new(0, 112, 1, -60), BackgroundTransparency = 1}, main)
new("UIListLayout", {Padding = UDim.new(0, 5), SortOrder = Enum.SortOrder.LayoutOrder}, sidebar)

content = new("Frame", {Position = UDim2.fromOffset(130, 50), Size = UDim2.new(1, -140, 1, -60), BackgroundTransparency = 1}, main)

local opener = new("TextButton", {
	Name = "Opener", AnchorPoint = Vector2.new(0.5, 0), Position = UDim2.new(0.5, 0, 0, 6), Size = UDim2.fromOffset(84, 22),
	BackgroundColor3 = C.bg, BackgroundTransparency = 0.25, Text = "GIGAMENU", TextColor3 = C.text,
	Font = Enum.Font.GothamBold, TextSize = 10, BorderSizePixel = 0, Visible = false,
}, gui)
corner(opener, 11)

function setMenuVisible(v)
	main.Visible = v
	opener.Visible = not v
end
closeBtn.MouseButton1Click:Connect(function() setMenuVisible(false) end)
opener.MouseButton1Click:Connect(function() setMenuVisible(true) end)

-- свернуть в полоску
local minimized = false
local function setMinimized(v)
	minimized = v
	if v then
		sidebar.Visible = false
		content.Visible = false
	end
	TweenService:Create(main, TweenInfo.new(0.18, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
		Size = UDim2.fromOffset(W, v and HEAD_H or H),
	}):Play()
	if not v then
		task.delay(0.18, function()
			if not minimized then
				sidebar.Visible = true
				content.Visible = true
			end
		end)
	end
end
minBtn.MouseButton1Click:Connect(function() setMinimized(not minimized) end)
end -- window

---------------------------------------------------------------
-- GUI: страницы и элементы
---------------------------------------------------------------
local PAGE_ORDER = {"Presets", "Customization", "Underglow", "Test", "Test2"}
local pages = {}
for _, name in ipairs(PAGE_ORDER) do
	local p = new("ScrollingFrame", {
		Name = name, Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0,
		ScrollBarThickness = 3, ScrollBarImageColor3 = RGB(70, 110, 130), ScrollBarImageTransparency = 0.3,
		CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, Visible = false,
	}, content)
	new("UIListLayout", {Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder}, p)
	new("UIPadding", {PaddingBottom = UDim.new(0, 8), PaddingRight = UDim.new(0, 8)}, p)
	pages[name] = p
end
local presetsPage, customPage, glowPage = pages.Presets, pages.Customization, pages.Underglow
local testPage, test2Page = pages.Test, pages.Test2

local sideButtons = {}
local function showPage(name)
	for n, p in pairs(pages) do p.Visible = (n == name) end
	for n, b in pairs(sideButtons) do
		local active = (n == name)
		b.BackgroundColor3 = active and C.tabOn or C.tabOff
		b.TextColor3 = active and C.text or C.dim
	end
	testVisible = (name == "Test")
	if testVisible then
		if refreshTestList then refreshTestList(false) end
	elseif closeTestPanel then
		closeTestPanel()
	end
end

for i, name in ipairs(PAGE_ORDER) do
	local b = new("TextButton", {
		Size = UDim2.new(1, 0, 0, 29), BackgroundColor3 = C.tabOff, Text = name, TextColor3 = C.dim,
		Font = Enum.Font.GothamMedium, TextSize = 11, AutoButtonColor = false, BorderSizePixel = 0, LayoutOrder = i,
	}, sidebar)
	corner(b, 7)
	sideButtons[name] = b
	b.MouseButton1Click:Connect(function() showPage(name) end)
end

local function sectionTitle(parent, text)
	return new("TextLabel", {
		Size = UDim2.new(1, 0, 0, 18), BackgroundTransparency = 1, Text = "|  " .. text,
		TextColor3 = C.head, Font = Enum.Font.GothamBold, TextSize = 10,
		TextXAlignment = Enum.TextXAlignment.Left, LayoutOrder = bump(parent),
	}, parent)
end

local function presetButton(parent, text, callback)
	local b = new("TextButton", {
		Size = UDim2.new(1, 0, 0, 35), BackgroundColor3 = C.row, Text = text, TextColor3 = C.text,
		Font = Enum.Font.Gotham, TextSize = 12, TextXAlignment = Enum.TextXAlignment.Left,
		AutoButtonColor = false, BorderSizePixel = 0, LayoutOrder = bump(parent),
	}, parent)
	corner(b, 7)
	new("UIPadding", {PaddingLeft = UDim.new(0, 12)}, b)
	b.MouseEnter:Connect(function() b.BackgroundColor3 = C.rowHi end)
	b.MouseLeave:Connect(function() b.BackgroundColor3 = C.row end)
	b.MouseButton1Click:Connect(callback)
	return b
end

local function row(parent, text)
	local r = new("Frame", {
		Size = UDim2.new(1, 0, 0, 40), BackgroundColor3 = C.row, BorderSizePixel = 0, LayoutOrder = bump(parent),
	}, parent)
	corner(r, 7)
	local l = new("TextLabel", {
		Size = UDim2.new(1, -150, 1, 0), Position = UDim2.fromOffset(12, 0), BackgroundTransparency = 1,
		Text = text, TextColor3 = C.text, Font = Enum.Font.Gotham, TextSize = 12,
		TextXAlignment = Enum.TextXAlignment.Left,
	}, r)
	return r, l
end

local function makeToggle(r, initial, callback)
	local b = new("TextButton", {
		AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0.5, 0), Size = UDim2.fromOffset(55, 22),
		Font = Enum.Font.GothamBold, TextSize = 10, AutoButtonColor = false, BorderSizePixel = 0,
	}, r)
	corner(b, 6)
	local on = initial
	local function render()
		b.Text = on and "ON" or "OFF"
		b.BackgroundColor3 = on and C.accent or C.btn
		b.TextColor3 = on and WHITE or C.dim
	end
	render()
	b.MouseButton1Click:Connect(function()
		on = not on
		render()
		callback(on)
	end)
	return {set = function(v) on = v render() end}
end

-- строка выбора цвета: swatch + EDIT, внутри разворачивается пикер R/G/B + SAVE + RAINBOW
local closeOpenPicker = nil

local function colorRow(parent, page, labelText, target, getColor, onSave)
	local wrap = new("Frame", {
		Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = bump(parent),
	}, parent)
	new("UIListLayout", {Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder}, wrap)

	local r = new("Frame", {Size = UDim2.new(1, 0, 0, 40), BackgroundColor3 = C.row, BorderSizePixel = 0, LayoutOrder = 1}, wrap)
	corner(r, 7)
	new("TextLabel", {
		Size = UDim2.new(1, -150, 1, 0), Position = UDim2.fromOffset(12, 0), BackgroundTransparency = 1,
		Text = labelText, TextColor3 = C.text, Font = Enum.Font.Gotham, TextSize = 12,
		TextXAlignment = Enum.TextXAlignment.Left,
	}, r)
	local sw = new("Frame", {
		AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -74, 0.5, 0), Size = UDim2.fromOffset(43, 18),
		BackgroundColor3 = getColor(), BorderSizePixel = 0,
	}, r)
	corner(sw, 5)
	local editBtn = new("TextButton", {
		AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0.5, 0), Size = UDim2.fromOffset(55, 22),
		BackgroundColor3 = C.btn, Text = "EDIT", TextColor3 = RGB(200, 205, 212), Font = Enum.Font.GothamBold, TextSize = 10,
		AutoButtonColor = false,
	}, r)
	corner(editBtn, 6)

	local panel = new("Frame", {
		Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundColor3 = C.row,
		BorderSizePixel = 0, Visible = false, LayoutOrder = 2,
	}, wrap)
	corner(panel, 7)
	new("UIPadding", {
		PaddingTop = UDim.new(0, 8), PaddingBottom = UDim.new(0, 8), PaddingLeft = UDim.new(0, 8), PaddingRight = UDim.new(0, 8),
	}, panel)
	new("UIListLayout", {Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder}, panel)

	local picker = makeRGBSliders(panel, page, function(c) sw.BackgroundColor3 = c end, 1)

	local saveBtn = new("TextButton", {
		Size = UDim2.new(1, 0, 0, 26), BackgroundColor3 = C.accent, Text = "SAVE", TextColor3 = WHITE,
		Font = Enum.Font.GothamBold, TextSize = 10, AutoButtonColor = true, LayoutOrder = 4,
	}, panel)
	corner(saveBtn, 6)
	local rainbowBtn = new("TextButton", {
		Size = UDim2.new(1, 0, 0, 26), BackgroundColor3 = C.btn, Text = "RAINBOW", TextColor3 = C.text,
		Font = Enum.Font.GothamBold, TextSize = 10, AutoButtonColor = true, LayoutOrder = 5,
	}, panel)
	corner(rainbowBtn, 6)

	local function renderRainbow()
		if rainbow[target] then
			rainbowBtn.Text = "STOP"
			rainbowBtn.BackgroundColor3 = C.red
			rainbowBtn.TextColor3 = WHITE
		else
			rainbowBtn.Text = "RAINBOW"
			rainbowBtn.BackgroundColor3 = C.btn
			rainbowBtn.TextColor3 = C.text
		end
	end

	local function close()
		panel.Visible = false
		editBtn.Text = "EDIT"
		sw.BackgroundColor3 = getColor()
		if closeOpenPicker == close then closeOpenPicker = nil end
	end

	editBtn.MouseButton1Click:Connect(function()
		if panel.Visible then
			close()
			return
		end
		if closeOpenPicker then closeOpenPicker() end
		closeOpenPicker = close
		picker.set(getColor())
		sw.BackgroundColor3 = getColor()
		renderRainbow()
		panel.Visible = true
		editBtn.Text = "CLOSE"
	end)

	saveBtn.MouseButton1Click:Connect(function()
		local c = picker.get()
		if rainbow[target] then setRainbow(target, false) end
		onSave(c)
		panel.Visible = false
		editBtn.Text = "EDIT"
		sw.BackgroundColor3 = c
		if closeOpenPicker == close then closeOpenPicker = nil end
	end)

	rainbowBtn.MouseButton1Click:Connect(function()
		setRainbow(target, not rainbow[target])
		renderRainbow()
	end)

	table.insert(ui.refreshers, function()
		if not panel.Visible then sw.BackgroundColor3 = getColor() end
		renderRainbow()
	end)
end

---------------------------------------------------------------
-- СТРАНИЦА: PRESETS (без Imba)
---------------------------------------------------------------
local warnLabel = new("TextLabel", {
	Size = UDim2.new(1, 0, 0, 34), BackgroundColor3 = RGB(60, 28, 28), BorderSizePixel = 0, TextWrapped = true,
	TextColor3 = RGB(255, 170, 160), Font = Enum.Font.Gotham, TextSize = 10, Visible = false, LayoutOrder = 0,
	TextXAlignment = Enum.TextXAlignment.Left,
}, presetsPage)
corner(warnLabel, 7)
new("UIPadding", {PaddingLeft = UDim.new(0, 10), PaddingRight = UDim.new(0, 10)}, warnLabel)

sectionTitle(presetsPage, "READY PRESETS")
for _, p in ipairs(READY_PRESETS) do
	presetButton(presetsPage, p.name, function() applyPreset(p) end)
end

---------------------------------------------------------------
-- СТРАНИЦА: CUSTOMIZATION
---------------------------------------------------------------
sectionTitle(customPage, "MODIFICATIONS")

do
	local r = row(customPage, "Auto Wheelie [" .. CONFIG.WHEELIE_KEY.Name .. "]")
	wheelieToggleUI = makeToggle(r, false, function(on) setWheelie(on) end)
end

do
	local r = row(customPage, "Freeze Scooter [" .. (CONFIG.FREEZE_KEY.Name:gsub("^Left", "")) .. "]")
	freezeToggleUI = makeToggle(r, false, function(on)
		if not setFreeze(on) then
			freezeToggleUI.set(false)
			notify("Sit on the scooter first")
		end
	end)
end

local wbButton
do
	local r = row(customPage, "Wheelie Bar")
	wbButton = new("TextButton", {
		AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0.5, 0), Size = UDim2.fromOffset(55, 22),
		BackgroundColor3 = C.accent, BackgroundTransparency = 1, Text = "N/A", TextColor3 = C.dim,
		Font = Enum.Font.GothamBold, TextSize = 10, AutoButtonColor = false, BorderSizePixel = 0,
	}, r)
	corner(wbButton, 6)
	wbButton.MouseButton1Click:Connect(function()
		ensure()
		if #groups.WheelieBar > 0 then return end
		if not isRiding() then
			notify("Sit on the scooter to add the wheelie bar")
			return
		end
		if buildWheelieBar() then
			state.wheelieBar = true
			ensure()
			if wbButtonUpdate then wbButtonUpdate() end
			notify("Wheelie bar added")
		end
	end)
end

wbButtonUpdate = function()
	if not wbButton then return end
	if #groups.WheelieBar > 0 then
		wbButton.Text = "DONE"
		wbButton.BackgroundTransparency = 1
		wbButton.TextColor3 = C.text
	elseif riding then
		wbButton.Text = "ADD"
		wbButton.BackgroundTransparency = 0
		wbButton.BackgroundColor3 = C.accent
		wbButton.TextColor3 = WHITE
	else
		wbButton.Text = "N/A"
		wbButton.BackgroundTransparency = 1
		wbButton.TextColor3 = C.dim
	end
end

do
	local r = row(customPage, "Light Trail")
	local t = makeToggle(r, state.trail, function(on)
		state.trail = on
		buildTrail()
	end)
	table.insert(ui.refreshers, function() t.set(state.trail) end)
end

do
	local r = row(customPage, "Rainbow Kukirin G4")
	local t = makeToggle(r, false, function(on) setRainbow("All", on) end)
	table.insert(ui.refreshers, function() t.set(rainbow.All == true) end)
end

local function colorOf(target)
	return savedColor(target) or (target == "Smoke" and WHITE) or groupDisplayColor(target)
end

local function saveColor(target, c)
	if target == "Trail" then
		state.trailColor = c
		if trail.obj then trail.obj.Color = ColorSequence.new(c) end
	elseif target == "Underglow" then
		state.glowColor = c
		setGlowColorRaw(c)
	elseif target == "Smoke" then
		state.smokeColor = c
		setSmokeColor(c)
	else
		state.colors[target] = c
		for _, p in ipairs(groups[target] or {}) do enforcePart(p) end
	end
end

local COLOR_ROWS = {
	{"Trail Color", "Trail"},
	{"Suspension Color", "Suspension"},
	{"Handlebar Color", "Handlebar"},
	{"Wheelie Bar Color", "WheelieBar"},
	{"Burnout Smoke Color", "Smoke"},
}
for _, cr in ipairs(COLOR_ROWS) do
	local target = cr[2]
	colorRow(customPage, customPage, cr[1], target, function() return colorOf(target) end, function(c) saveColor(target, c) end)
end

---------------------------------------------------------------
-- СТРАНИЦА: UNDERGLOW
---------------------------------------------------------------
sectionTitle(glowPage, "UNDERGLOW LIGHTING")
do
	local r = row(glowPage, "Underglow State")
	local t = makeToggle(r, state.underglow, function(on)
		state.underglow = on
		buildGlow()
	end)
	table.insert(ui.refreshers, function() t.set(state.underglow) end)
end
colorRow(glowPage, glowPage, "Underglow Color", "Underglow", function() return state.glowColor end, function(c) saveColor("Underglow", c) end)

---------------------------------------------------------------
-- СТРАНИЦА: TEST (детали самоката)
---------------------------------------------------------------
do -- Test page
local testRows = {}   -- [путь] = {row, swatch, order, title}
local testToken, testSig = 0, ""
local selectedKey = nil

sectionTitle(testPage, "SCOOTER PARTS")
new("TextLabel", {
	Size = UDim2.new(1, 0, 0, 28), BackgroundTransparency = 1, TextWrapped = true,
	Text = "Click a part: it lights up on the scooter and you can change its color live.",
	TextColor3 = C.dim, Font = Enum.Font.Gotham, TextSize = 10, TextXAlignment = Enum.TextXAlignment.Left,
	TextYAlignment = Enum.TextYAlignment.Top, LayoutOrder = bump(testPage),
}, testPage)
local testSearch = textBox(testPage, {
	Size = UDim2.new(1, 0, 0, 28), PlaceholderText = "Search parts...", LayoutOrder = bump(testPage),
})
local testEmpty = new("TextLabel", {
	Size = UDim2.new(1, 0, 0, 30), BackgroundTransparency = 1, TextWrapped = true, Visible = false,
	Text = "No parts found. Make sure the scooter exists in workspace.",
	TextColor3 = RGB(255, 170, 160), Font = Enum.Font.Gotham, TextSize = 10, TextXAlignment = Enum.TextXAlignment.Left,
	LayoutOrder = bump(testPage),
}, testPage)

local testPanel = new("Frame", {
	Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundColor3 = C.rowHi,
	BorderSizePixel = 0, Visible = false, LayoutOrder = 100000,
}, testPage)
corner(testPanel, 7)
new("UIPadding", {
	PaddingTop = UDim.new(0, 8), PaddingBottom = UDim.new(0, 8), PaddingLeft = UDim.new(0, 8), PaddingRight = UDim.new(0, 8),
}, testPanel)
new("UIListLayout", {Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder}, testPanel)
local testTitle = new("TextLabel", {
	Size = UDim2.new(1, 0, 0, 16), BackgroundTransparency = 1, Text = "", TextColor3 = C.text,
	Font = Enum.Font.GothamBold, TextSize = 11, TextXAlignment = Enum.TextXAlignment.Left,
	TextTruncate = Enum.TextTruncate.AtEnd, LayoutOrder = 1,
}, testPanel)

local function currentKeyColor(key)
	local c = state.partColors[key]
	if c then return c end
	local list = partsByKey[key]
	local p = list and list[1]
	return p and p.Color or WHITE
end

local testPicker = makeRGBSliders(testPanel, testPage, function(c)
	if not selectedKey then return end
	state.partColors[selectedKey] = c
	for _, p in ipairs(partsByKey[selectedKey] or {}) do enforcePart(p) end
	local r = testRows[selectedKey]
	if r then r.swatch.BackgroundColor3 = c end
end, 2)

local testBtns = new("Frame", {Size = UDim2.new(1, 0, 0, 26), BackgroundTransparency = 1, LayoutOrder = 6}, testPanel)
local resetBtn = new("TextButton", {
	Size = UDim2.new(0.5, -3, 1, 0), BackgroundColor3 = C.btn, Text = "RESET", TextColor3 = C.text,
	Font = Enum.Font.GothamBold, TextSize = 10, AutoButtonColor = true, BorderSizePixel = 0,
}, testBtns)
corner(resetBtn, 6)
local closePanelBtn = new("TextButton", {
	Position = UDim2.new(0.5, 3, 0, 0), Size = UDim2.new(0.5, -3, 1, 0), BackgroundColor3 = C.accent, Text = "CLOSE",
	TextColor3 = WHITE, Font = Enum.Font.GothamBold, TextSize = 10, AutoButtonColor = true, BorderSizePixel = 0,
}, testBtns)
corner(closePanelBtn, 6)

closeTestPanel = function()
	selectedKey = nil
	testPanel.Visible = false
	clearHighlight()
end

local function selectKey(key)
	local r = testRows[key]
	if not r then return end
	if selectedKey == key then
		closeTestPanel()
		return
	end
	selectedKey = key
	testPanel.LayoutOrder = r.order + 1
	testTitle.Text = r.title
	testPicker.set(currentKeyColor(key))
	testPanel.Visible = true
	highlightKey(key)
end

resetBtn.MouseButton1Click:Connect(function()
	if not selectedKey then return end
	state.partColors[selectedKey] = nil
	repaintAll()
	local c = currentKeyColor(selectedKey)
	testPicker.set(c)
	local r = testRows[selectedKey]
	if r then r.swatch.BackgroundColor3 = c end
end)
closePanelBtn.MouseButton1Click:Connect(function() closeTestPanel() end)

local function applyFilter()
	local q = testSearch.Text:lower()
	for key, r in pairs(testRows) do
		r.row.Visible = (q == "" or key:lower():find(q, 1, true) ~= nil)
	end
	if selectedKey and testRows[selectedKey] and not testRows[selectedKey].row.Visible then
		closeTestPanel()
	end
end
testSearch:GetPropertyChangedSignal("Text"):Connect(applyFilter)

refreshTestList = function(force)
	ensure()
	local sig = #keyList .. ":" .. #allParts
	if not force and sig == testSig then return end
	testSig = sig
	testToken += 1
	local token = testToken
	local keepSel = selectedKey
	closeTestPanel()
	for _, r in pairs(testRows) do r.row:Destroy() end
	testRows = {}
	testEmpty.Visible = (#keyList == 0)

	local keys = keyList
	task.spawn(function()
		for i, key in ipairs(keys) do
			if token ~= testToken then return end
			local list = partsByKey[key]
			if list and #list > 0 then
				local leaf = key:match("([^/]+)$") or key
				local title = leaf .. (#list > 1 and ("  x" .. #list) or "")
				local grp = partGroupOf[list[1]] or "no group"
				local order = 100 + i * 2

				local b = new("TextButton", {
					Size = UDim2.new(1, 0, 0, 38), BackgroundColor3 = C.row, Text = "", AutoButtonColor = false,
					BorderSizePixel = 0, LayoutOrder = order,
				}, testPage)
				corner(b, 7)
				new("TextLabel", {
					Position = UDim2.fromOffset(12, 3), Size = UDim2.new(1, -70, 0, 18), BackgroundTransparency = 1,
					Text = title, TextColor3 = C.text, Font = Enum.Font.Gotham, TextSize = 12,
					TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd,
				}, b)
				new("TextLabel", {
					Position = UDim2.fromOffset(12, 21), Size = UDim2.new(1, -70, 0, 13), BackgroundTransparency = 1,
					Text = grp .. "  ·  " .. key, TextColor3 = C.dim, Font = Enum.Font.Gotham, TextSize = 9,
					TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd,
				}, b)
				local sw = new("Frame", {
					AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0.5, 0), Size = UDim2.fromOffset(40, 16),
					BackgroundColor3 = currentKeyColor(key), BorderSizePixel = 0,
				}, b)
				corner(sw, 5)
				b.MouseEnter:Connect(function() b.BackgroundColor3 = C.rowHi end)
				b.MouseLeave:Connect(function() b.BackgroundColor3 = C.row end)
				b.MouseButton1Click:Connect(function() selectKey(key) end)

				testRows[key] = {row = b, swatch = sw, order = order, title = title}
			end
			if i % 15 == 0 then task.wait() end
		end
		if token == testToken then
			applyFilter()
			if keepSel and testRows[keepSel] then selectKey(keepSel) end
		end
	end)
end

end -- Test page

---------------------------------------------------------------
-- СТРАНИЦА: TEST2 (PUBLIC / PRIVATE пресеты + Cloud key)
---------------------------------------------------------------
do -- Test2 page
local privatePresets = {}
local FILE = "GIGAMENU_presets.json"

local function savePrivate()
	if typeof(writefile) ~= "function" then return end
	pcall(function() writefile(FILE, HttpService:JSONEncode(privatePresets)) end)
end

local function loadPrivate()
	if typeof(isfile) ~= "function" or typeof(readfile) ~= "function" then return end
	local ok, data = pcall(function()
		if isfile(FILE) then return HttpService:JSONDecode(readfile(FILE)) end
		return nil
	end)
	if ok and type(data) == "table" then
		for _, e in ipairs(data) do
			if type(e) == "table" and type(e.name) == "string" and type(e.data) == "table" then
				table.insert(privatePresets, e)
			end
		end
	end
end
loadPrivate()

-- строка пресета: название слева, кнопки справа. actions = {{текст, ширина, функция, фон?, цвет текста?}, ...}
local function presetRow(parent, title, actions)
	local r = new("Frame", {Size = UDim2.new(1, 0, 0, 38), BackgroundColor3 = C.row, BorderSizePixel = 0, LayoutOrder = bump(parent)}, parent)
	corner(r, 7)
	local x, total = -8, 0
	for i = #actions, 1, -1 do
		local a = actions[i]
		local b = new("TextButton", {
			AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, x, 0.5, 0), Size = UDim2.fromOffset(a[2], 22),
			BackgroundColor3 = a[4] or C.btn, Text = a[1], TextColor3 = a[5] or C.text, Font = Enum.Font.GothamBold,
			TextSize = 10, AutoButtonColor = true, BorderSizePixel = 0,
		}, r)
		corner(b, 6)
		b.MouseButton1Click:Connect(a[3])
		x = x - a[2] - 4
		total = total + a[2] + 4
	end
	new("TextLabel", {
		Size = UDim2.new(1, -(total + 20), 1, 0), Position = UDim2.fromOffset(12, 0), BackgroundTransparency = 1,
		Text = title, TextColor3 = C.text, Font = Enum.Font.Gotham, TextSize = 12,
		TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd,
	}, r)
	return r
end

local keyBox -- создаётся ниже
local function showKey(key, name)
	if not key then
		notify("Could not build the key")
		return
	end
	keyBox.Text = key
	if typeof(setclipboard) == "function" then
		pcall(setclipboard, key)
		notify("Key copied: " .. name)
	else
		notify("Key is in the box below: select it and press Ctrl+C")
	end
	pcall(function()
		keyBox:CaptureFocus()
		task.defer(function()
			keyBox.SelectionStart = 1
			keyBox.CursorPosition = #keyBox.Text + 1
		end)
	end)
end

-- PUBLIC: общие (готовые) пресеты
sectionTitle(test2Page, "PUBLIC PRESETS")
for _, p in ipairs(READY_PRESETS) do
	if p.kind == "classic" then
		presetRow(test2Page, p.name, {
			{"APPLY", 52, function()
				applyPreset(p)
				notify("Applied: " .. p.name)
			end, C.accent, WHITE},
			{"KEY", 38, function() showKey(encodeKey(classicToData(p)), p.name) end},
		})
	end
end

-- PRIVATE: свои пресеты
sectionTitle(test2Page, "PRIVATE PRESETS")

local nameBox = textBox(test2Page, {Size = UDim2.new(1, -78, 0, 28), PlaceholderText = "Name for current look..."})
local saveRow = new("Frame", {Size = UDim2.new(1, 0, 0, 28), BackgroundTransparency = 1, LayoutOrder = bump(test2Page)}, test2Page)
nameBox.Parent = saveRow
local saveNowBtn = new("TextButton", {
	AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 0), Size = UDim2.fromOffset(72, 28),
	BackgroundColor3 = C.accent, Text = "SAVE", TextColor3 = WHITE, Font = Enum.Font.GothamBold, TextSize = 10,
	AutoButtonColor = true, BorderSizePixel = 0,
}, saveRow)
corner(saveNowBtn, 6)

local privHolder = new("Frame", {
	Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = bump(test2Page),
}, test2Page)
new("UIListLayout", {Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder}, privHolder)

local rebuildPrivate
rebuildPrivate = function()
	for _, ch in ipairs(privHolder:GetChildren()) do
		if not ch:IsA("UIListLayout") then ch:Destroy() end
	end
	counters[privHolder] = 0
	if #privatePresets == 0 then
		new("TextLabel", {
			Size = UDim2.new(1, 0, 0, 24), BackgroundTransparency = 1, Text = "No private presets yet.",
			TextColor3 = C.dim, Font = Enum.Font.Gotham, TextSize = 10, TextXAlignment = Enum.TextXAlignment.Left,
			LayoutOrder = bump(privHolder),
		}, privHolder)
		return
	end
	for i, e in ipairs(privatePresets) do
		presetRow(privHolder, e.name, {
			{"APPLY", 52, function()
				applyData(e.data)
				notify("Applied: " .. e.name)
			end, C.accent, WHITE},
			{"KEY", 38, function() showKey(encodeKey(e.data), e.name) end},
			{"X", 26, function()
				table.remove(privatePresets, i)
				savePrivate()
				rebuildPrivate()
			end, C.red, WHITE},
		})
	end
end
rebuildPrivate()

saveNowBtn.MouseButton1Click:Connect(function()
	local name = trim(nameBox.Text):sub(1, 24)
	if name == "" then name = "Preset " .. (#privatePresets + 1) end
	table.insert(privatePresets, {name = name, data = captureData(name)})
	savePrivate()
	rebuildPrivate()
	nameBox.Text = ""
	notify("Saved: " .. name)
end)

-- CLOUD KEY: импорт / экспорт
sectionTitle(test2Page, "CLOUD KEY")

local importRow = new("Frame", {Size = UDim2.new(1, 0, 0, 28), BackgroundTransparency = 1, LayoutOrder = bump(test2Page)}, test2Page)
local importBox = textBox(importRow, {Size = UDim2.new(1, -78, 1, 0), PlaceholderText = "Paste a Cloud key here..."})
local importBtn = new("TextButton", {
	AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 0), Size = UDim2.fromOffset(72, 28),
	BackgroundColor3 = C.accent, Text = "IMPORT", TextColor3 = WHITE, Font = Enum.Font.GothamBold, TextSize = 10,
	AutoButtonColor = true, BorderSizePixel = 0,
}, importRow)
corner(importBtn, 6)

keyBox = textBox(test2Page, {
	Size = UDim2.new(1, 0, 0, 28), PlaceholderText = "Press KEY on a preset to get its Cloud key here",
	LayoutOrder = bump(test2Page),
})

importBtn.MouseButton1Click:Connect(function()
	local d, err = decodeKey(importBox.Text)
	if not d then
		notify("Invalid key: " .. tostring(err))
		return
	end
	local name = (type(d.n) == "string" and trim(d.n) ~= "") and trim(d.n):sub(1, 24) or "Imported"
	applyData(d)
	table.insert(privatePresets, {name = name, data = captureData(name)})
	savePrivate()
	rebuildPrivate()
	importBox.Text = ""
	notify("Imported: " .. name)
end)

end -- Test2 page

---------------------------------------------------------------
-- статус (показывается только если что-то не так) + кнопка Wheelie Bar
---------------------------------------------------------------
updateStatus = function()
	local txt
	if not scooter then
		txt = 'Самокат не найден: workspace["' .. CONFIG.SCOOTER_NAME .. '"]. Проверь CONFIG.SCOOTER_NAME.'
	else
		local total = 0
		for _, g in ipairs(GROUP_ORDER) do total = total + #groups[g] end
		if total == 0 then txt = "Детали самоката не распознаны — смотри Output (F9), впиши имена в PART_NAMES." end
	end
	warnLabel.Visible = (txt ~= nil)
	if txt then warnLabel.Text = txt end
	if wbButtonUpdate then wbButtonUpdate() end
end
updateStatus()

---------------------------------------------------------------
-- ВВОД: F = вилли, Shift = заморозка, RightShift = меню
---------------------------------------------------------------
connect(UIS.InputBegan, function(input, processed)
	if processed then return end
	if input.UserInputType ~= Enum.UserInputType.Keyboard then return end

	if input.KeyCode == CONFIG.MENU_KEY then
		setMenuVisible(not main.Visible)
	elseif input.KeyCode == CONFIG.WHEELIE_KEY then
		if CONFIG.WHEELIE_HOLD then
			setWheelie(true)
		else
			setWheelie(not wheelie.on)
		end
	elseif input.KeyCode == CONFIG.FREEZE_KEY then
		if setFreeze(not freeze.on) then
			notify(freeze.on and "Scooter frozen" or "Scooter unfrozen")
		else
			notify("Sit on the scooter first")
		end
	end
end)

connect(UIS.InputEnded, function(input)
	if CONFIG.WHEELIE_HOLD
		and input.UserInputType == Enum.UserInputType.Keyboard
		and input.KeyCode == CONFIG.WHEELIE_KEY then
		setWheelie(false)
	end
end)

---------------------------------------------------------------
-- СТАРТ
---------------------------------------------------------------
showPage("Presets")
warn("[GIGAMENU v2.0] loaded — by zyru | " .. CONFIG.WHEELIE_KEY.Name .. " = wheelie, "
	.. CONFIG.FREEZE_KEY.Name .. " = freeze, " .. CONFIG.MENU_KEY.Name .. " = menu")

Features & Information

its ai vibecode pasta Start only when you're on the scooter! (works only on G4) - запускайте только когда встали на скутер (работает только на Kukirin g4)

Community Comments (0)

No comments yet for this script.

More Scripts for (UPDATE) Drive A Kukirin!