Initial MVP: drivable RX-7 vertical slice + server-authoritative systems
CI / quality (push) Failing after 5m53s

- Rojo + Wally project; ProfileStore (loleris) for safe saves
- Shared: GameConfig, Cars (RX-7 free starter), ProfileTemplate, Remotes
- Server: DataManager, Economy, CarManager, RaceManager, MapBuilder, AntiCheat
- Client: neon HUD + Garage/Dealership UI (buy/equip/upgrade), race/daily
- Procedural Tokyo expressway (spawn pad, finish line, neon pylons, kill walls)
- CI workflow (Gitea/GitHub), Makefile, selene+stylua clean, rojo builds
- Docs: README, GDD, realistic solo ROADMAP
This commit is contained in:
2026-07-21 23:30:53 -04:00
commit 9a2f2d4dc9
24 changed files with 1986 additions and 0 deletions
+391
View File
@@ -0,0 +1,391 @@
--!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.
]]
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Shared = ReplicatedStorage.Shared
local Remotes = require(Shared.Remotes)
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
-- UI helpers ----------------------------------------------------------------
local NEON_PINK = Color3.fromRGB(255, 40, 120)
local NEON_BLUE = Color3.fromRGB(40, 160, 255)
local PANEL_BG = Color3.fromRGB(16, 16, 24)
local function make(class: string, props: { [string]: any }, parent: Instance?): any
local inst = Instance.new(class)
for k, v in pairs(props) do
(inst :: any)[k] = v
end
if parent then
inst.Parent = parent
end
return inst
end
local function corner(inst: Instance, radius: number)
make("UICorner", { CornerRadius = UDim.new(0, radius) }, inst)
end
local screen = make("ScreenGui", {
Name = "FnF_UI",
ResetOnSpawn = false,
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
}, playerGui)
-- ===== HUD (top-left): cash / rep / car ====================================
local hud = make("Frame", {
Name = "HUD",
Size = UDim2.fromOffset(260, 96),
Position = UDim2.fromOffset(16, 16),
BackgroundColor3 = PANEL_BG,
BackgroundTransparency = 0.15,
BorderSizePixel = 0,
}, screen)
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),
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,
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 ===================================================
local bar = make("Frame", {
Name = "ActionBar",
Size = UDim2.fromOffset(430, 52),
Position = UDim2.new(0.5, -215, 1, -70),
BackgroundTransparency = 1,
}, screen)
make("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
Padding = UDim.new(0, 10),
SortOrder = Enum.SortOrder.LayoutOrder,
}, bar)
local function button(text: string, color: Color3, order: number): TextButton
local b = make("TextButton", {
Size = UDim2.fromOffset(100, 44),
BackgroundColor3 = PANEL_BG,
BackgroundTransparency = 0.1,
Font = Enum.Font.GothamBold,
TextColor3 = color,
TextSize = 15,
Text = text,
AutoButtonColor = true,
LayoutOrder = order,
BorderSizePixel = 0,
}, bar)
corner(b, 8)
make("UIStroke", { Color = color, Thickness = 1.2, Transparency = 0.4 }, b)
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)
-- ===== Toast (transient messages) ==========================================
local toast = make("TextLabel", {
Name = "Toast",
Size = UDim2.fromOffset(360, 34),
Position = UDim2.new(0.5, -180, 1, -120),
BackgroundColor3 = PANEL_BG,
BackgroundTransparency = 0.1,
Font = Enum.Font.GothamMedium,
TextColor3 = Color3.fromRGB(255, 255, 255),
TextSize = 15,
Text = "",
Visible = false,
BorderSizePixel = 0,
}, screen)
corner(toast, 8)
local toastToken = 0
local function showToast(msg: string)
toast.Text = msg
toast.Visible = true
toastToken += 1
local myToken = toastToken
task.delay(2.6, 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),
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)
make("TextLabel", {
Size = UDim2.new(1, 0, 0, 40),
BackgroundTransparency = 1,
Font = Enum.Font.GothamBold,
TextColor3 = Color3.fromRGB(255, 255, 255),
TextSize = 20,
Text = "GARAGE & DEALERSHIP",
}, garage)
local closeBtn = make("TextButton", {
Size = UDim2.fromOffset(32, 32),
Position = UDim2.new(1, -40, 0, 6),
BackgroundColor3 = NEON_PINK,
Font = Enum.Font.GothamBold,
TextColor3 = Color3.fromRGB(255, 255, 255),
TextSize = 18,
Text = "X",
BorderSizePixel = 0,
}, garage)
corner(closeBtn, 8)
local list = make("ScrollingFrame", {
Size = UDim2.new(1, -24, 1, -56),
Position = UDim2.fromOffset(12, 46),
BackgroundTransparency = 1,
BorderSizePixel = 0,
ScrollBarThickness = 6,
CanvasSize = UDim2.new(0, 0, 0, 0),
AutomaticCanvasSize = Enum.AutomaticSize.Y,
}, garage)
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
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
local row = make("Frame", {
Size = UDim2.new(1, 0, 0, 72),
BackgroundColor3 = Color3.fromRGB(26, 26, 38),
BorderSizePixel = 0,
LayoutOrder = i,
}, list)
corner(row, 8)
make("TextLabel", {
Size = UDim2.new(0.6, 0, 0, 22),
Position = UDim2.fromOffset(12, 8),
BackgroundTransparency = 1,
Font = Enum.Font.GothamBold,
TextColor3 = Color3.fromRGB(255, 255, 255),
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 16,
Text = string.format("T%d %s", car.Tier, car.Name),
}, row)
make("TextLabel", {
Size = UDim2.new(0.6, 0, 0, 18),
Position = UDim2.fromOffset(12, 32),
BackgroundTransparency = 1,
Font = Enum.Font.Gotham,
TextColor3 = Color3.fromRGB(170, 170, 185),
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 12,
Text = string.format(
"%s | SPD %d ACC %d HND %d",
car.Model,
car.Stats.Speed,
car.Stats.Acceleration,
car.Stats.Handling
),
}, row)
-- Primary action button (Equip / Buy / locked) --------------------------
local action = make("TextButton", {
Size = UDim2.fromOffset(120, 30),
Position = UDim2.new(1, -132, 0, 10),
Font = Enum.Font.GothamBold,
TextSize = 13,
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
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.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")
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()
end
end
rDataUpdated.OnClientEvent:Connect(applyData)
-- ===== Wire buttons ========================================================
garageBtn.MouseButton1Click:Connect(function()
garage.Visible = not garage.Visible
if garage.Visible then
rebuildGarage()
end
end)
closeBtn.MouseButton1Click:Connect(function()
garage.Visible = false
end)
spawnBtn.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
end)
print("[FnF] Client UI ready.")