Per user direction, the MVP is now simply driving cars on the expressway. - Removed RaceManager and Economy modules entirely - Slimmed Remotes to DataUpdated / SpawnCar / EquipCar - ProfileTemplate now just remembers the last driven car (v2) - Cars: dropped price/rep/upgrade fields; kept identity + driving stats - CarManager: stat-based tuning only; RX-7 allowlist via GameConfig.canDrive - DataManager: assign default car on join; downgrade RX-7 if de-allowlisted - Client: replaced garage/economy UI with a simple car picker + respawn - Docs: GDD/ROADMAP/README rewritten for drive-only; added docs/MESH_SWAP.md - Gates: stylua clean, selene 0/0, rojo builds
This commit is contained in:
+74
-178
@@ -1,10 +1,12 @@
|
||||
--!strict
|
||||
--[[
|
||||
init.client.luau (CLIENT ENTRY POINT)
|
||||
Builds the HUD (cash / rep / car), a Garage+Dealership panel, and a Daily
|
||||
button — all in code so there's nothing to build in Studio. The client is
|
||||
purely presentational: it shows server-provided data and sends requests.
|
||||
It never decides money or ownership.
|
||||
init.client.luua (CLIENT ENTRY POINT)
|
||||
MVP scope: JUST DRIVING. Minimal UI:
|
||||
- A small HUD showing the current car name.
|
||||
- A "CARS" button opening a picker to switch cars (RX-7 shown only if you
|
||||
can drive it — the server enforces this regardless).
|
||||
- A "RESPAWN CAR" button to re-spawn your car if you flip or fall off.
|
||||
The client is presentational; the server owns everything.
|
||||
]]
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
@@ -17,17 +19,11 @@ local Cars = require(Shared.Cars)
|
||||
local player = Players.LocalPlayer
|
||||
local playerGui = player:WaitForChild("PlayerGui")
|
||||
|
||||
-- Remotes -------------------------------------------------------------------
|
||||
local rDataUpdated = Remotes.get("DataUpdated") :: RemoteEvent
|
||||
local rSpawnCar = Remotes.get("SpawnCar") :: RemoteEvent
|
||||
local rEquipCar = Remotes.get("EquipCar") :: RemoteFunction
|
||||
local rBuyCar = Remotes.get("BuyCar") :: RemoteFunction
|
||||
local rUpgradeCar = Remotes.get("UpgradeCar") :: RemoteFunction
|
||||
local rClaimDaily = Remotes.get("ClaimDaily") :: RemoteFunction
|
||||
local rStartRace = Remotes.get("StartRace") :: RemoteFunction
|
||||
|
||||
-- Local cache of the latest server snapshot ---------------------------------
|
||||
local latest: any = nil
|
||||
local currentCarId = ""
|
||||
|
||||
-- UI helpers ----------------------------------------------------------------
|
||||
local NEON_PINK = Color3.fromRGB(255, 40, 120)
|
||||
@@ -55,10 +51,10 @@ local screen = make("ScreenGui", {
|
||||
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
}, playerGui)
|
||||
|
||||
-- ===== HUD (top-left): cash / rep / car ====================================
|
||||
-- HUD: current car ----------------------------------------------------------
|
||||
local hud = make("Frame", {
|
||||
Name = "HUD",
|
||||
Size = UDim2.fromOffset(260, 96),
|
||||
Size = UDim2.fromOffset(260, 52),
|
||||
Position = UDim2.fromOffset(16, 16),
|
||||
BackgroundColor3 = PANEL_BG,
|
||||
BackgroundTransparency = 0.15,
|
||||
@@ -67,44 +63,22 @@ local hud = make("Frame", {
|
||||
corner(hud, 10)
|
||||
make("UIStroke", { Color = NEON_PINK, Thickness = 1.5, Transparency = 0.3 }, hud)
|
||||
|
||||
local cashLabel = make("TextLabel", {
|
||||
Size = UDim2.new(1, -20, 0, 28),
|
||||
Position = UDim2.fromOffset(12, 8),
|
||||
local carLabel = make("TextLabel", {
|
||||
Size = UDim2.new(1, -20, 1, 0),
|
||||
Position = UDim2.fromOffset(12, 0),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextColor3 = Color3.fromRGB(120, 255, 170),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextSize = 20,
|
||||
Text = "$ --",
|
||||
}, hud)
|
||||
|
||||
local repLabel = make("TextLabel", {
|
||||
Size = UDim2.new(1, -20, 0, 22),
|
||||
Position = UDim2.fromOffset(12, 38),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextColor3 = NEON_BLUE,
|
||||
TextColor3 = Color3.fromRGB(230, 230, 240),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextSize = 16,
|
||||
Text = "REP --",
|
||||
}, hud)
|
||||
|
||||
local carLabel = make("TextLabel", {
|
||||
Size = UDim2.new(1, -20, 0, 20),
|
||||
Position = UDim2.fromOffset(12, 64),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.Gotham,
|
||||
TextColor3 = Color3.fromRGB(220, 220, 230),
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextSize = 14,
|
||||
Text = "Car: --",
|
||||
}, hud)
|
||||
|
||||
-- ===== Bottom action bar ===================================================
|
||||
-- Bottom action bar ---------------------------------------------------------
|
||||
local bar = make("Frame", {
|
||||
Name = "ActionBar",
|
||||
Size = UDim2.fromOffset(430, 52),
|
||||
Position = UDim2.new(0.5, -215, 1, -70),
|
||||
Size = UDim2.fromOffset(230, 52),
|
||||
Position = UDim2.new(0.5, -115, 1, -70),
|
||||
BackgroundTransparency = 1,
|
||||
}, screen)
|
||||
make("UIListLayout", {
|
||||
@@ -116,7 +90,7 @@ make("UIListLayout", {
|
||||
|
||||
local function button(text: string, color: Color3, order: number): TextButton
|
||||
local b = make("TextButton", {
|
||||
Size = UDim2.fromOffset(100, 44),
|
||||
Size = UDim2.fromOffset(105, 44),
|
||||
BackgroundColor3 = PANEL_BG,
|
||||
BackgroundTransparency = 0.1,
|
||||
Font = Enum.Font.GothamBold,
|
||||
@@ -132,12 +106,10 @@ local function button(text: string, color: Color3, order: number): TextButton
|
||||
return b
|
||||
end
|
||||
|
||||
local garageBtn = button("GARAGE", NEON_BLUE, 1)
|
||||
local spawnBtn = button("SPAWN CAR", Color3.fromRGB(120, 255, 170), 2)
|
||||
local raceBtn = button("RACE", NEON_PINK, 3)
|
||||
local dailyBtn = button("DAILY", Color3.fromRGB(255, 210, 80), 4)
|
||||
local carsBtn = button("CARS", NEON_BLUE, 1)
|
||||
local respawnBtn = button("RESPAWN", Color3.fromRGB(120, 255, 170), 2)
|
||||
|
||||
-- ===== Toast (transient messages) ==========================================
|
||||
-- Toast ---------------------------------------------------------------------
|
||||
local toast = make("TextLabel", {
|
||||
Name = "Toast",
|
||||
Size = UDim2.fromOffset(360, 34),
|
||||
@@ -159,25 +131,25 @@ local function showToast(msg: string)
|
||||
toast.Visible = true
|
||||
toastToken += 1
|
||||
local myToken = toastToken
|
||||
task.delay(2.6, function()
|
||||
task.delay(2.4, function()
|
||||
if toastToken == myToken then
|
||||
toast.Visible = false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- ===== Garage / Dealership panel ===========================================
|
||||
local garage = make("Frame", {
|
||||
Name = "Garage",
|
||||
Size = UDim2.fromOffset(560, 420),
|
||||
Position = UDim2.new(0.5, -280, 0.5, -210),
|
||||
-- Car picker panel ----------------------------------------------------------
|
||||
local picker = make("Frame", {
|
||||
Name = "CarPicker",
|
||||
Size = UDim2.fromOffset(520, 400),
|
||||
Position = UDim2.new(0.5, -260, 0.5, -200),
|
||||
BackgroundColor3 = PANEL_BG,
|
||||
BackgroundTransparency = 0.05,
|
||||
Visible = false,
|
||||
BorderSizePixel = 0,
|
||||
}, screen)
|
||||
corner(garage, 12)
|
||||
make("UIStroke", { Color = NEON_BLUE, Thickness = 1.5, Transparency = 0.25 }, garage)
|
||||
corner(picker, 12)
|
||||
make("UIStroke", { Color = NEON_BLUE, Thickness = 1.5, Transparency = 0.25 }, picker)
|
||||
|
||||
make("TextLabel", {
|
||||
Size = UDim2.new(1, 0, 0, 40),
|
||||
@@ -185,8 +157,8 @@ make("TextLabel", {
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||
TextSize = 20,
|
||||
Text = "GARAGE & DEALERSHIP",
|
||||
}, garage)
|
||||
Text = "PICK A CAR",
|
||||
}, picker)
|
||||
|
||||
local closeBtn = make("TextButton", {
|
||||
Size = UDim2.fromOffset(32, 32),
|
||||
@@ -197,7 +169,7 @@ local closeBtn = make("TextButton", {
|
||||
TextSize = 18,
|
||||
Text = "X",
|
||||
BorderSizePixel = 0,
|
||||
}, garage)
|
||||
}, picker)
|
||||
corner(closeBtn, 8)
|
||||
|
||||
local list = make("ScrollingFrame", {
|
||||
@@ -208,36 +180,27 @@ local list = make("ScrollingFrame", {
|
||||
ScrollBarThickness = 6,
|
||||
CanvasSize = UDim2.new(0, 0, 0, 0),
|
||||
AutomaticCanvasSize = Enum.AutomaticSize.Y,
|
||||
}, garage)
|
||||
}, picker)
|
||||
make("UIListLayout", { Padding = UDim.new(0, 8), SortOrder = Enum.SortOrder.LayoutOrder }, list)
|
||||
|
||||
-- Build one row per car; content depends on owned/equipped/affordable --------
|
||||
local function rebuildGarage()
|
||||
if not latest then
|
||||
return
|
||||
end
|
||||
local function rebuildPicker()
|
||||
for _, child in ipairs(list:GetChildren()) do
|
||||
if child:IsA("Frame") then
|
||||
child:Destroy()
|
||||
end
|
||||
end
|
||||
|
||||
local ownedIds: { [string]: any } = {}
|
||||
for _, o in ipairs(latest.OwnedCars) do
|
||||
ownedIds[o.Id] = o
|
||||
end
|
||||
|
||||
for i, car in ipairs(Cars.List) do
|
||||
local owned = ownedIds[car.Id]
|
||||
local equipped = latest.EquippedCarId == car.Id
|
||||
|
||||
-- Hide restricted (allowlist-only) cars from anyone who doesn't own them.
|
||||
if car.Restricted and not owned then
|
||||
-- Hide the restricted RX-7 unless it's the car we're currently driving
|
||||
-- (i.e. the server already granted it to us). The server enforces this
|
||||
-- regardless of what the client shows.
|
||||
if car.Restricted and currentCarId ~= car.Id then
|
||||
continue
|
||||
end
|
||||
|
||||
local isCurrent = currentCarId == car.Id
|
||||
local row = make("Frame", {
|
||||
Size = UDim2.new(1, 0, 0, 72),
|
||||
Size = UDim2.new(1, 0, 0, 64),
|
||||
BackgroundColor3 = Color3.fromRGB(26, 26, 38),
|
||||
BorderSizePixel = 0,
|
||||
LayoutOrder = i,
|
||||
@@ -245,7 +208,7 @@ local function rebuildGarage()
|
||||
corner(row, 8)
|
||||
|
||||
make("TextLabel", {
|
||||
Size = UDim2.new(0.6, 0, 0, 22),
|
||||
Size = UDim2.new(0.62, 0, 0, 22),
|
||||
Position = UDim2.fromOffset(12, 8),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.GothamBold,
|
||||
@@ -256,7 +219,7 @@ local function rebuildGarage()
|
||||
}, row)
|
||||
|
||||
make("TextLabel", {
|
||||
Size = UDim2.new(0.6, 0, 0, 18),
|
||||
Size = UDim2.new(0.62, 0, 0, 16),
|
||||
Position = UDim2.fromOffset(12, 32),
|
||||
BackgroundTransparency = 1,
|
||||
Font = Enum.Font.Gotham,
|
||||
@@ -272,125 +235,58 @@ local function rebuildGarage()
|
||||
),
|
||||
}, row)
|
||||
|
||||
-- Primary action button (Equip / Buy / locked) --------------------------
|
||||
local action = make("TextButton", {
|
||||
Size = UDim2.fromOffset(120, 30),
|
||||
Position = UDim2.new(1, -132, 0, 10),
|
||||
Size = UDim2.fromOffset(120, 34),
|
||||
Position = UDim2.new(1, -132, 0.5, -17),
|
||||
Font = Enum.Font.GothamBold,
|
||||
TextSize = 13,
|
||||
TextSize = 14,
|
||||
BorderSizePixel = 0,
|
||||
}, row)
|
||||
corner(action, 6)
|
||||
|
||||
if owned then
|
||||
if equipped then
|
||||
action.Text = "EQUIPPED"
|
||||
action.BackgroundColor3 = Color3.fromRGB(40, 60, 55)
|
||||
action.TextColor3 = Color3.fromRGB(120, 255, 170)
|
||||
else
|
||||
action.Text = "EQUIP"
|
||||
action.BackgroundColor3 = NEON_BLUE
|
||||
action.TextColor3 = Color3.fromRGB(0, 0, 0)
|
||||
action.MouseButton1Click:Connect(function()
|
||||
local ok = rEquipCar:InvokeServer(car.Id)
|
||||
if ok then
|
||||
showToast("Equipped " .. car.Name)
|
||||
end
|
||||
end)
|
||||
end
|
||||
if isCurrent then
|
||||
action.Text = "DRIVING"
|
||||
action.BackgroundColor3 = Color3.fromRGB(40, 60, 55)
|
||||
action.TextColor3 = Color3.fromRGB(120, 255, 170)
|
||||
else
|
||||
local affordable = latest.Cash >= car.Price and latest.Rep >= car.RepRequired
|
||||
action.Text = string.format("BUY $%d", car.Price)
|
||||
action.BackgroundColor3 = affordable and Color3.fromRGB(120, 255, 170) or Color3.fromRGB(60, 40, 48)
|
||||
action.TextColor3 = affordable and Color3.fromRGB(0, 0, 0) or Color3.fromRGB(200, 120, 130)
|
||||
action.Text = "DRIVE"
|
||||
action.BackgroundColor3 = NEON_BLUE
|
||||
action.TextColor3 = Color3.fromRGB(0, 0, 0)
|
||||
action.MouseButton1Click:Connect(function()
|
||||
local res = rBuyCar:InvokeServer(car.Id)
|
||||
if res and res.ok then
|
||||
showToast("Purchased " .. car.Name .. "!")
|
||||
elseif res then
|
||||
showToast(res.reason or "Cannot buy")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Upgrade button (only for owned cars) ----------------------------------
|
||||
if owned then
|
||||
local upg = make("TextButton", {
|
||||
Size = UDim2.fromOffset(120, 24),
|
||||
Position = UDim2.new(1, -132, 0, 42),
|
||||
Font = Enum.Font.GothamMedium,
|
||||
TextSize = 12,
|
||||
BackgroundColor3 = Color3.fromRGB(45, 45, 60),
|
||||
TextColor3 = Color3.fromRGB(255, 210, 80),
|
||||
BorderSizePixel = 0,
|
||||
Text = string.format(
|
||||
"UPGRADE (E%d/T%d/N%d)",
|
||||
owned.Upgrades.Engine,
|
||||
owned.Upgrades.Tires,
|
||||
owned.Upgrades.Nitro
|
||||
),
|
||||
}, row)
|
||||
corner(upg, 6)
|
||||
upg.MouseButton1Click:Connect(function()
|
||||
-- MVP: cycle Engine -> Tires -> Nitro whichever is lowest.
|
||||
local parts = { "Engine", "Tires", "Nitro" }
|
||||
table.sort(parts, function(a, b)
|
||||
return owned.Upgrades[a] < owned.Upgrades[b]
|
||||
end)
|
||||
local res = rUpgradeCar:InvokeServer(car.Id, parts[1])
|
||||
if res and res.ok then
|
||||
showToast("Upgraded " .. parts[1] .. "!")
|
||||
elseif res then
|
||||
showToast(res.reason or "Cannot upgrade")
|
||||
local ok = rEquipCar:InvokeServer(car.Id)
|
||||
if ok then
|
||||
showToast("Now driving " .. car.Name)
|
||||
else
|
||||
showToast("Can't drive that one")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== Data sync ===========================================================
|
||||
local function applyData(data: any)
|
||||
latest = data
|
||||
cashLabel.Text = string.format("$ %s", tostring(data.Cash))
|
||||
repLabel.Text = string.format("REP %s", tostring(data.Rep))
|
||||
local car = Cars.Get(data.EquippedCarId)
|
||||
carLabel.Text = "Car: " .. (car and car.Name or data.EquippedCarId)
|
||||
if garage.Visible then
|
||||
rebuildGarage()
|
||||
-- Data sync -----------------------------------------------------------------
|
||||
rDataUpdated.OnClientEvent:Connect(function(carId: string)
|
||||
currentCarId = carId
|
||||
local car = Cars.Get(carId)
|
||||
carLabel.Text = "Car: " .. (car and car.Name or carId)
|
||||
if picker.Visible then
|
||||
rebuildPicker()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
rDataUpdated.OnClientEvent:Connect(applyData)
|
||||
|
||||
-- ===== Wire buttons ========================================================
|
||||
garageBtn.MouseButton1Click:Connect(function()
|
||||
garage.Visible = not garage.Visible
|
||||
if garage.Visible then
|
||||
rebuildGarage()
|
||||
-- Buttons -------------------------------------------------------------------
|
||||
carsBtn.MouseButton1Click:Connect(function()
|
||||
picker.Visible = not picker.Visible
|
||||
if picker.Visible then
|
||||
rebuildPicker()
|
||||
end
|
||||
end)
|
||||
closeBtn.MouseButton1Click:Connect(function()
|
||||
garage.Visible = false
|
||||
picker.Visible = false
|
||||
end)
|
||||
spawnBtn.MouseButton1Click:Connect(function()
|
||||
respawnBtn.MouseButton1Click:Connect(function()
|
||||
rSpawnCar:FireServer()
|
||||
showToast("Spawning your car...")
|
||||
end)
|
||||
raceBtn.MouseButton1Click:Connect(function()
|
||||
local res = rStartRace:InvokeServer("expressway_timetrial")
|
||||
if res and res.ok then
|
||||
showToast("Race started! Reach the finish line!")
|
||||
elseif res then
|
||||
showToast(res.reason or "Cannot start race")
|
||||
end
|
||||
end)
|
||||
dailyBtn.MouseButton1Click:Connect(function()
|
||||
local res = rClaimDaily:InvokeServer()
|
||||
if res and res.ok then
|
||||
showToast(string.format("Daily: +$%d, +%d rep!", res.cash or 0, res.rep or 0))
|
||||
elseif res then
|
||||
showToast(res.reason or "Already claimed")
|
||||
end
|
||||
showToast("Respawning your car...")
|
||||
end)
|
||||
|
||||
print("[FnF] Client UI ready.")
|
||||
print("[FnF] Client UI ready (drive-only MVP).")
|
||||
|
||||
Reference in New Issue
Block a user