Garage
Complete garage system — tabbed menu, reactive vehicle stats, sub-menu actions, repair progress bar with animation and target zone.
1 min read
Full FiveM resource example. Demonstrates:
tab— “My Garage” / “Buy” / “Options” tabsstatwith reactivevalueandcolorbuttonwithpreviewpanelalert— purchase and repair confirmationsprogress— repair bar with ped animation and propnotify— result feedbacktarget_add_sphere— garage interaction zone
local LM = exports.LastMenu
-- ── Fake data (replace with your server data) ─────────────────────────────────
local playerVehicles = {
{ model = "adder", label = "Adder", fuel = 82, bodyHealth = 1000 },
{ model = "zentorno", label = "Zentorno", fuel = 41, bodyHealth = 740 },
{ model = "t20", label = "T20", fuel = 95, bodyHealth = 980 },
}
local vehiclesForSale = {
{ model = "sultan", label = "Sultan RS", price = 12000 },
{ model = "elegy2", label = "Elegy RH8", price = 95000 },
{ model = "banshee2", label = "Banshee 900R", price = 565000 },
}
local garageCoords = vector3(215.0, -810.0, 30.0)
-- ── Helpers ───────────────────────────────────────────────────────────────────
local function healthToColor(hp)
local pct = hp / 1000
if pct > 0.75 then return "#4ade80" end
if pct > 0.40 then return "#facc15" end
return "#f87171"
end
local function formatPrice(n) return string.format("%d $", n) end
-- ── Vehicle sub-menu ──────────────────────────────────────────────────────────
local function openVehicleMenu(vehicle)
LM:context(function(menu)
menu:title(vehicle.label)
menu:description("Vehicle options")
menu:animation("slideRight")
menu:stat("Fuel", {
value = function() return vehicle.fuel end,
max = 100,
suffix = "%",
color = "auto",
refresh = 1000,
})
menu:stat("Body", {
value = function() return vehicle.bodyHealth end,
max = 1000,
color = function() return healthToColor(vehicle.bodyHealth) end,
refresh = 800,
})
menu:separator()
menu:button("Retrieve vehicle", {
icon = "car",
arrow = true,
cb = function()
LM:alert(function(a)
a:title("Retrieve vehicle")
a:message("Spawn the " .. vehicle.label .. " in front of the garage?")
a:confirm("Retrieve", function()
LM:progress(function(p)
p:label("Retrieving vehicle…")
p:duration(3000)
p:icon("car")
p:anim({
dict = "anim@heists@ornate_bank@security_guard",
clip = "stand_fire_loop_pistol",
flag = 1,
})
p:confirm(function()
-- In production: RequestModel() + CreateVehicle()
LM:notify(function(n)
n:message(vehicle.label .. " retrieved!")
n:type("success"); n:duration(4000)
end)
end)
end)
end)
a:cancel("Cancel")
end)
end,
})
menu:button("Repair", {
icon = "wrench",
disabled = function() return vehicle.bodyHealth >= 1000 end,
badge = function()
if vehicle.bodyHealth >= 1000 then return "OK" end
return string.format("%d%%", math.floor(vehicle.bodyHealth / 10))
end,
refresh = 1000,
cb = function()
local cost = math.floor((1000 - vehicle.bodyHealth) * 0.8)
LM:alert(function(a)
a:title("Repair vehicle")
a:message(string.format("Repair the %s for %s?", vehicle.label, formatPrice(cost)))
a:confirm("Pay & Repair", function()
LM:progress(function(p)
p:label("Repairing…")
p:duration(5000)
p:cancelable(true)
p:icon("wrench")
p:anim({ dict = "amb@world_human_welding@male@base", clip = "base", flag = 1 })
p:prop({
model = "prop_tool_torch",
bone = 57005,
offset = vector3(0.0, 0.0, 0.0),
rot = vector3(0.0, 0.0, 0.0),
})
p:confirm(function()
vehicle.bodyHealth = 1000
LM:notify(function(n)
n:message(vehicle.label .. " repaired!")
n:type("success")
end)
end)
p:cancel(function()
LM:notify(function(n)
n:message("Repair cancelled.")
n:type("warning"); n:duration(2500)
end)
end)
end)
end)
a:cancel("Cancel")
end)
end,
})
menu:button("Sell", {
icon = "badge-dollar-sign",
confirm_hold = 2000,
hint = formatPrice(math.floor((vehicle.price or 50000) / 2)),
cb = function()
for i, v in ipairs(playerVehicles) do
if v.model == vehicle.model then table.remove(playerVehicles, i) break end
end
LM:notify(function(n)
n:message(vehicle.label .. " sold!")
n:type("success"); n:duration(5000)
end)
end,
})
menu:back("Back")
end)
end
-- ── Main garage menu ──────────────────────────────────────────────────────────
local function openGarageMenu()
LM:context(function(menu)
menu:title("Los Santos Garage")
menu:banner("https://i.imgur.com/placeholder.png")
menu:nav("both")
-- Tab: My Garage
menu:tab("My Garage", function(t)
if #playerVehicles == 0 then
t:header("No registered vehicle", { align = "center" })
return
end
for _, vehicle in ipairs(playerVehicles) do
local v = vehicle
t:button(v.label, {
icon = "car",
arrow = true,
badge = function()
return string.format("%d%%", math.floor(v.bodyHealth / 10))
end,
color = function() return healthToColor(v.bodyHealth) end,
refresh = 1000,
preview = {
title = v.label,
desc = "Personal vehicle",
stats = {
{ label = "Fuel", value = v.fuel, max = 100, color = "auto" },
{ label = "Body", value = v.bodyHealth, max = 1000, color = healthToColor(v.bodyHealth) },
},
},
cb = function() openVehicleMenu(v) end,
})
end
end, { icon = "car-front" })
-- Tab: Buy
menu:tab("Buy", function(t)
t:header("Available vehicles", { align = "center" })
for _, car in ipairs(vehiclesForSale) do
local c = car
t:button(c.label, {
icon = "shopping-cart",
hint = formatPrice(c.price),
arrow = true,
preview = {
title = c.label,
desc = "New vehicle — immediate delivery",
stats = { { label = "Price", value = c.price, max = 1000000 } },
},
cb = function()
LM:alert(function(a)
a:title("Buy " .. c.label)
a:message(string.format("Confirm purchase of %s for %s?",
c.label, formatPrice(c.price)))
a:confirm("Buy", function()
table.insert(playerVehicles, {
model = c.model, label = c.label,
fuel = 100, bodyHealth = 1000, price = c.price,
})
LM:notify(function(n)
n:message(c.label .. " purchased! Find it in My Garage.")
n:type("success"); n:duration(5000)
end)
end)
a:cancel("Cancel")
end)
end,
})
end
end, { icon = "store" })
-- Tab: Options
menu:tab("Options", function(t)
t:toggle("Garage notifications", {
default = true,
icon = "bell",
cb = function(val)
LM:notify(function(n)
n:message("Notifications " .. (val and "enabled" or "disabled"))
n:type(val and "success" or "info"); n:duration(2000)
end)
end,
})
t:slider("Alert distance (m)", {
min = 50, max = 500, step = 50, default = 150, suffix = " m",
icon = "radar",
})
end, { icon = "settings" })
end)
end
-- ── Target zone ───────────────────────────────────────────────────────────────
Citizen.CreateThread(function()
LM:target_add_sphere(garageCoords, 3.0, {
label = "Garage",
icon = "car-front",
actions = {
{ label = "Open garage", icon = "door-open", cb = function() openGarageMenu() end },
},
})
end)