Clothing: config files
These files stay readable in every build (escrow too), so you can change them. Shown as shipped in version 1.0.3.
shared.lua
lua
-- lumo-clothing | shared
Lumo = {}
-- Set to true to enable debug prints in the client and server consoles
Lumo.Debug = false
-- where thumbnails live (served by NUI)
Lumo.ImagesDir = 'web/images/catalog/clothing/'
-- panel defaults
Lumo.UIDefaultDock = 'left' -- 'left' | 'right'
Lumo.PanelWidthPct = 28 -- default width (%)
Lumo.PanelWidthOptions = { 28, 34, 42 } -- Compact / Comfort / Wide
-- Interaction Mode
-- "interact" = lumo-interact prompts, themed by lumo-ui (recommended)
-- "marker" = draw markers + press E
-- "none" = no markers/NPCs, open via command/event
-- "npc" and "qb-target" are legacy aliases of "interact".
Lumo.InteractionMode = "interact"
-- Spawn a shop attendant ped that the prompt attaches to. With this off the
-- prompt anchors to the shop coordinates instead.
Lumo.SpawnNPCs = true
-- Saved outfits per character. Caps an otherwise unbounded client-driven
-- INSERT; names are trimmed to Lumo.OutfitNameMax characters.
Lumo.MaxOutfits = 40
Lumo.OutfitNameMax = 32
-- ─────────────────────────────────────────────────────────────────────────────
-- Clothing ownership
-- Buying a garment records it against the character. Re-wearing something you
-- already own is free; everything else is charged normally.
-- ─────────────────────────────────────────────────────────────────────────────
Lumo.Ownership = {
enabled = true,
-- Fraction taken off a re-buy of something you already own. 1.0 = free.
rebuyDiscount = 1.0,
-- Seed a character's wardrobe from the clothes they are already wearing the
-- first time they are seen. Turn off only on a brand-new server.
grandfather = true,
-- Strip components from a saved outfit that the character does not own.
-- Off by default: it is a hard rule and will surprise existing players.
strictOutfits = false,
-- Categories ownership never applies to (underwear, skin, etc).
ignoreCategories = { arms = true, undershirt = true, decals = true }
}
-- ─────────────────────────────────────────────────────────────────────────────
-- Condition & laundry
-- Worn garments accumulate dirt over time and damage from harm. Both are 0-100.
-- ─────────────────────────────────────────────────────────────────────────────
Lumo.Laundry = {
enabled = false,
-- Dirt gained per tick of wear, and how long a tick is (minutes).
dirtPerTick = 1,
tickMinutes = 6,
-- One-off dirt/damage events.
onDeath = { dirt = 22, damage = 14 },
onDamage = { dirt = 3, damage = 4 },
onRevive = { dirt = 10, damage = 6 },
-- Above this, the character reads as visibly unkempt to other resources.
scruffyAt = 55,
filthyAt = 80,
-- Try to swap to a dirtier texture variant of the same garment. Only applies
-- when the garment actually has another variant; purely cosmetic.
visualSwap = true,
-- Wash pricing.
washBase = 25, -- flat fee
washPerGarment = 8, -- per dirty garment
repairPerPoint = 3, -- mending charge per point of damage
washSeconds = 8,
-- Counter interaction: the target eye (qb-target / ox_target through
-- lumo-bridge) when one is running, lumo-interact prompts otherwise.
useTarget = true,
-- Actual walk-in MLO: Jerry's Laundromat by levdevlev (MIT).
-- Attendant position verified by the server owner; prompt sits at ped waist height.
-- No marker is registered unless the required map is running.
locations = {
{ id = 'jerrys_laundromat', label = "Jerry's Laundromat",
coords = vector4(897.31, -1039.78, 35.25, 5.96),
ped = 's_f_m_sweatshop_01', promptOffset = vector3(0.0, 0.0, 0.0),
resource = 'lev_laundromat', distance = 2.0, blip = 100, color = 3 }
}
}
-- Attendant service: hand your clothes over and an employee walks to a free
-- machine and runs the cycle, instead of you standing at it yourself.
-- The location's own desk ped does the work: you undress and hand her the
-- basket, she loads a machine and starts it, waits while it runs, and brings
-- your clothes back folded for you to put on again.
Lumo.Laundry.attendant = {
enabled = true,
-- Only used when a location has no desk ped of its own.
model = 's_f_y_shop_low',
-- How long the machine runs once loaded. The walk, hand-over and folding
-- come on top of this.
washSeconds = 20,
-- Added on top of the normal wash price for the service.
serviceFee = 10,
-- Stay within this many metres of the counter to be handed the clean load.
-- Farther away it is still finished and charged, just not handed over.
handoverDistance = 6.0,
-- Seconds to wait for the ped to reach a spot before she steps over to it.
walkTimeout = 12,
-- Carried to the machine, and back once it is washed and folded.
props = { dirty = 'prop_washing_basket_01', clean = 'prop_cs_t_shirt_pile' },
-- You undress to hand the load over and dress again when it comes back.
-- Visual only: the saved skin is never touched, and the clothing menus stay
-- shut until you are dressed.
undress = true,
-- The running machine. The MLO's washers are part of the building, so the
-- cycle is drawn on top of one: a looping sound (a file in web/sounds/,
-- heard within `range` metres), a glow in the drum, and laundry tumbling
-- behind the door glass. `doorHeight` is the drum's centre above the floor.
machine = {
sound = 'washer_loop.wav', volume = 0.5, range = 14.0,
light = true,
tumble = { 'prop_cs_tshirt_ball_01', 'prop_cs_tshirt_ball_01', 'prop_cs_tshirt_ball_01' },
doorHeight = 0.6,
},
}
-- Washing machine spots per laundry location: where the attendant stands to
-- load a machine, facing it.
--
-- These are interior coordinates inside an MLO, and guessing them from an
-- offset does not work. Stand in front of a machine, face it, and run
-- `/washmachine <location id>` as an admin. The spot is saved to
-- laundry_machines.json and used at once, no restart; `/washmachine
-- <location id> clear` forgets that location's saved spots. Spots listed here
-- are used as well, ahead of the saved ones.
--
-- With none at all, the attendant turns to the machine behind her counter.
Lumo.Laundry.machines = {
-- jerrys_laundromat = {
-- vector4(x, y, z, heading),
-- },
}
-- Cleaning consumable: the second route. Slower, partial, and it costs an item,
-- but it works anywhere -- useful if you are far from a laundromat.
Lumo.Laundry.item = {
enabled = true,
name = 'detergent',
-- A garment is cleaned only if its dirt is at or below this. Heavier soiling
-- still needs a real machine, which is what keeps laundromats worth visiting.
maxDirt = 60,
seconds = 12,
-- Sold at the laundromat counter. 24/7 stores stock it through qb-shops.
price = 15,
}
-- Soap: wash yourself and what you have on, anywhere. Foam on the body and a
-- standing scrub; it takes dirt and blood off the skin and dirt out of the
-- clothes. It does not mend -- wear still needs the laundromat.
Lumo.Laundry.soap = {
enabled = true,
name = 'soap',
seconds = 10,
-- Garments dirtier than this are left for a machine. 100 = anything.
maxDirt = 100,
}
-- Foam on the body while scrubbing with soap or detergent; first that loads.
Lumo.Laundry.foam = {
candidates = {
{ asset = 'cut_trevor3', effect = 'cs_trev3_soap_suds', scale = 0.35 },
{ asset = 'core', effect = 'water_splash_ped_bubbles', scale = 0.6 },
},
}
-- How dirt shows on you, on top of the texture swap. From scruffyAt: grime on
-- skin and clothes (a ped damage pack). From filthyAt: flies around you, which
-- other players see too.
Lumo.Laundry.dirtyLook = {
enabled = true,
grime = true, grimePack = 'SCR_Dumpster', scruffyGrime = 0.4, filthyGrime = 1.0,
flies = true, fliesScale = 0.7,
}
-- Washing particle effects (suds at the machine door). Several candidates
-- because asset names vary between game builds; the first that loads is used,
-- and if none do the wash still runs without visuals rather than failing.
Lumo.Laundry.ptfx = {
enabled = true,
candidates = {
{ asset = 'cut_trevor3', effect = 'cs_trev3_soap_suds', scale = 0.5 },
{ asset = 'core', effect = 'water_splash_ped_bubbles' },
{ asset = 'core', effect = 'water_splash_ped_in' },
{ asset = 'scr_carwash', effect = 'ent_amb_car_wash_jet' },
{ asset = 'core', effect = 'ent_amb_steam' },
},
scale = 1.2,
}
function Lumo.IsLaundryLocationAvailable(location)
return type(location) == 'table' and location.enabled ~= false
and (not location.resource or GetResourceState(location.resource) == 'started')
end
-- ─────────────────────────────────────────────────────────────────────────────
-- Brand tiers. Tier drives the badge shown on an item and the default social
-- read of the garment. Owners set a margin per shop on top of listed prices.
-- ─────────────────────────────────────────────────────────────────────────────
Lumo.Brands = {
enabled = true,
tiers = {
[1] = { label = 'Discount', color = '#8b9096' },
[2] = { label = 'Everyday', color = '#8fd0ff' },
[3] = { label = 'Premium', color = '#b7ff3a' },
[4] = { label = 'Designer', color = '#ffb648' },
[5] = { label = 'Couture', color = '#ff7ad9' }
},
-- Bounds an owner's margin multiplier so nobody lists a $2,000,000 sock.
marginMin = 0.50,
marginMax = 3.00
}
-- ─────────────────────────────────────────────────────────────────────────────
-- Clothing as evidence. Police resources call the exports; nothing here runs
-- on its own.
-- ─────────────────────────────────────────────────────────────────────────────
Lumo.Evidence = {
enabled = true,
-- Jobs allowed to pull a description or search past records.
jobs = { police = true, sheriff = true, sasp = true, doj = true },
-- A mask hides identity: the description says so and omits the face.
maskConceals = true,
-- How long a stored description stays queryable (days). 0 = forever.
retainDays = 30
}
-- ─────────────────────────────────────────────────────────────────────────────
-- Clothing as inventory items
-- One qb-core item per wearable slot; the actual garment lives in metadata, so
-- thousands of drawables need only a dozen item definitions. Every conversion
-- is server-authoritative against the saved skin, which is what stops a client
-- banking the same garment twice.
-- ─────────────────────────────────────────────────────────────────────────────
Lumo.ClothingItems = {
enabled = true,
-- Wearing a garment cut for the other body type shows a different drawable
-- entirely, so it is blocked rather than silently wrong.
enforceGender = true,
-- A take-off is refused outright when there is no room to carry the garment.
-- The check runs before the saved skin is touched, so a full inventory leaves
-- the character dressed exactly as they were rather than half-converted.
checkRoomFirst = true,
-- Garments taken off become owned by whoever ends up holding them.
transferOwnership = true,
-- slot key -> qb-core item name. Slot keys are the shop's UI categories.
-- Add a row here and to qb-core/shared/items.lua to support another slot.
items = {
mask = 'clothing_mask',
jacket = 'clothing_jacket',
undershirt = 'clothing_shirt',
vests = 'clothing_vest',
pants = 'clothing_pants',
shoes = 'clothing_shoes',
bags = 'clothing_bag',
neck = 'clothing_necklace',
hats = 'clothing_hat',
glasses = 'clothing_glasses',
ear = 'clothing_earrings',
watch = 'clothing_watch',
bracelet = 'clothing_bracelet'
},
-- Friendly names used when a garment has no catalog label.
slotLabels = {
mask = 'Mask', jacket = 'Jacket', undershirt = 'Shirt', vests = 'Vest',
pants = 'Trousers', shoes = 'Shoes', bags = 'Bag', neck = 'Necklace',
hats = 'Hat', glasses = 'Glasses', ear = 'Earrings', watch = 'Watch',
bracelet = 'Bracelet'
},
-- Every name any other resource, command or menu might use for a slot.
-- Lumo.Slot(name) resolves through this, so the radial menu's 'Top', a
-- /tshirt command and the shop's own 'undershirt' all land on one slot.
aliases = {
mask = 'mask', masks = 'mask', facemask = 'mask',
jacket = 'jacket', jackets = 'jacket', top = 'jacket', tops = 'jacket',
torso = 'jacket', torso2 = 'jacket', coat = 'jacket',
undershirt = 'undershirt', shirt = 'undershirt', tshirt = 'undershirt',
['t-shirt'] = 'undershirt', tee = 'undershirt',
vest = 'vests', vests = 'vests', bulletproof = 'vests',
pants = 'pants', pant = 'pants', trousers = 'pants', legs = 'pants',
shoes = 'shoes', shoe = 'shoes', boots = 'shoes', feet = 'shoes',
bag = 'bags', bags = 'bags', bagoff = 'bags', backpack = 'bags',
parachute = 'bags',
neck = 'neck', necklace = 'neck', neckl = 'neck', chain = 'neck',
accessory = 'neck', tie = 'neck',
hat = 'hats', hats = 'hats', cap = 'hats', helmet = 'hats',
glasses = 'glasses', glass = 'glasses', sunglasses = 'glasses',
specs = 'glasses', eyewear = 'glasses',
ear = 'ear', ears = 'ear', earring = 'ear', earrings = 'ear',
earpiece = 'ear',
watch = 'watch', watches = 'watch',
bracelet = 'bracelet', bracelets = 'bracelet', wristband = 'bracelet'
},
-- Chat command aliases lumo-clothing registers itself. Names already owned by
-- another started resource are skipped at registration time, so nothing here
-- silently overwrites qb-radialmenu's /shirt or /pants.
commands = {
'takeoff', 'wear', 'undress',
'top', 'jacket', 'torso',
'shirt', 'tshirt', 'undershirt',
'pants', 'trousers', 'shoes',
'vest', 'bag', 'backpack', 'mask',
'neck', 'necklace', 'hat',
'glasses', 'sunglasses',
'ear', 'earrings', 'watch', 'bracelet'
},
-- Command names another resource is expected to own. lumo-clothing does not
-- register these; that resource routes them here instead (see the adapter in
-- qb-radialmenu/client/clothing.lua).
reservedCommands = {
['qb-radialmenu'] = {
'top', 'shirt', 'pants', 'shoes', 'vest', 'bag', 'bagoff', 'mask',
'hat', 'glasses', 'ear', 'neck', 'watch', 'bracelet'
}
},
-- Per-slot dressing animation. Played client-side before the server is asked
-- to convert, so the change lands on the frame the hands finish moving.
anims = {
jacket = { dict = 'clothingtie', clip = 'try_tie_negative_a', ms = 1200 },
undershirt = { dict = 'clothingtie', clip = 'try_tie_negative_a', ms = 1200 },
vests = { dict = 'clothingtie', clip = 'try_tie_negative_a', ms = 1200 },
neck = { dict = 'clothingtie', clip = 'try_tie_positive_a', ms = 1600 },
pants = { dict = 're@construction', clip = 'out_of_breath', ms = 1300 },
shoes = { dict = 'random@domestic', clip = 'pickup_low', ms = 1200 },
bags = { dict = 'anim@heists@ornate_bank@grab_cash', clip = 'intro', ms = 1400 },
mask = { dict = 'mp_masks@standard_car@ds@', clip = 'put_on_mask', ms = 900 },
hats = { dict = 'missheist_agency2ahelmet', clip = 'take_off_helmet_stand', ms = 1200 },
glasses = { dict = 'clothingspecs', clip = 'take_off', ms = 1200 },
ear = { dict = 'mp_cp_stolen_tut', clip = 'b_think', ms = 900 },
watch = { dict = 'nmt_3_rcm-10', clip = 'cs_nigel_dual-10', ms = 1100 },
bracelet = { dict = 'nmt_3_rcm-10', clip = 'cs_nigel_dual-10', ms = 1100 }
}
}
--- Canonical slot key for any name a caller might use, or nil if it is not a
--- wearable slot. Shared so the client, the server and every adapter agree.
function Lumo.Slot(name)
if type(name) ~= 'string' then return nil end
local key = name:lower():gsub('%s+', '')
local aliases = (Lumo.ClothingItems or {}).aliases or {}
return aliases[key]
end
--- True when the item system is on and the slot is one it manages.
function Lumo.IsItemSlot(name)
if not (Lumo.ClothingItems and Lumo.ClothingItems.enabled) then return false end
local slot = Lumo.Slot(name)
return slot ~= nil and (Lumo.ClothingItems.items or {})[slot] ~= nil
end
--- True only when a resource folder really named `name` is started. The
--- fxmanifest `provide`s esx_skin and skinchanger, which makes
--- GetResourceState(name) answer 'started' for lumo-clothing itself.
function Lumo.RealResource(name)
if GetResourceState(name) ~= 'started' then return false end
for i = 0, GetNumResources() - 1 do
if GetResourceByFindIndex(i) == name then return true end
end
return false
end
-- ─────────────────────────────────────────────────────────────────────────────
-- Hair fit: hair ties and the hat/helmet clipping fix
--
-- GTA decides how much hair a head prop cuts from `pedalternatevariations.meta`
-- in the ped's DLC. Vanilla hair is described there; add-on and EUP hair
-- usually is not, which is why it pokes through hats. There is no runtime
-- native to change that metadata, so the working fix — the one every headgear
-- script uses — is to swap the hair drawable for a shorter one while a head
-- prop is worn, and swap it back afterwards.
--
-- The swap is applied to the ped only. The character's real hair stays in the
-- saved skin, so nothing here can permanently change someone's hairstyle.
-- ─────────────────────────────────────────────────────────────────────────────
Lumo.Hair = {
enabled = true,
-- Item that ties hair back. Toggling is free and instant.
tieItem = 'hairtie',
tieAnim = { dict = 'anim@mp_player_intincarcelebrate', clip = 'celebrate' },
tieSeconds = 2,
-- Automatically fit hair under a head prop (hat/helmet) as it goes on.
autoFitUnderHats = true,
-- Bike/flight helmets the game puts on by itself count as head props too.
suppressVehicleHelmets = false,
-- Hair drawables that clip badly. Anything not listed is left alone, so an
-- untouched server behaves exactly as it does today.
-- [hairDrawable] = tiedDrawable
-- `fallback` is used for a hair that clips but has no mapping of its own.
tied = {
male = {
fallback = 6, -- short crop
[7] = 6, [8] = 6, [9] = 6, [10] = 6, [11] = 6,
[12] = 6, [13] = 6, [14] = 6, [15] = 6, [16] = 6
},
female = {
fallback = 4, -- tied-back bun
[7] = 4, [8] = 4, [9] = 4, [10] = 4, [11] = 4,
[12] = 4, [13] = 4, [14] = 4, [15] = 4, [16] = 4,
[17] = 4, [18] = 4, [19] = 4, [20] = 4
}
},
-- Hair drawables that clip through head props. Empty means "trust the map
-- above": any hair with an entry in `tied` is treated as clipping.
clipsUnderHats = {}
}
-- ─────────────────────────────────────────────────────────────────────────────
-- Property wardrobes. lumo-housing (or anything else) calls
-- exports['lumo-clothing']:OpenPropertyWardrobe(propertyId).
-- ─────────────────────────────────────────────────────────────────────────────
Lumo.PropertyWardrobe = {
enabled = true,
-- Outfits saved at a property are still global; this only gates the panel.
requireInside = true
}
-- Performance Mode
-- shared.lua (near top)
Lumo.PerformanceMode = GetConvarInt('lumo:perf', 0) == 1 or Lumo.PerformanceMode
-- NPC Settings – only clothing shops now
Lumo.NPCs = {
clothing = {
model = "s_f_m_shop_high",
scenario = "WORLD_HUMAN_STAND_IMPATIENT",
frozen = true,
invincible = true
},
barber = {
-- common barber model in GTA V
model = "s_m_m_hairdress_01",
scenario = "WORLD_HUMAN_STAND_IMPATIENT",
frozen = true,
invincible = true
},
tattoo = {
-- tattoo artist model
model = "u_m_y_tattoo_01",
scenario = "WORLD_HUMAN_CLIPBOARD",
frozen = true,
invincible = true
}
}
-- Single UI theme. The names below are the custom properties the NUI reads,
-- so editing a value here restyles the panel without rebuilding the bundle.
-- Anything left out keeps the stylesheet default.
Lumo.DefaultTheme = 'lumo-terminal'
Lumo.Themes = {
["lumo-terminal"] = {
label = "Lumo Terminal",
vars = {
-- lime accent on near-black, matched to qb-inventory
["--accent"] = "#b7ff3a",
["--accent-hover"] = "#c9ff66",
["--accent-dim"] = "rgba(183, 255, 58, 0.5)",
["--accent-soft"] = "rgba(183, 255, 58, 0.10)",
["--accent-glow"] = "rgba(183, 255, 58, 0.30)",
["--accent-deep"] = "#6f9e18",
["--accent-text"] = "#0a1400",
["--panel-bg"] = "rgba(7, 10, 8, 0.90)",
["--panel-bg-2"] = "rgba(5, 7, 6, 0.97)",
["--card-bg"] = "rgba(255, 255, 255, 0.035)",
["--card-bg-hover"] = "rgba(183, 255, 58, 0.10)",
["--btn-bg"] = "rgba(255, 255, 255, 0.05)",
["--btn-hover"] = "rgba(183, 255, 58, 0.12)",
["--border"] = "rgba(255, 255, 255, 0.10)",
["--border-strong"] = "rgba(183, 255, 58, 0.45)",
["--hairline"] = "rgba(255, 255, 255, 0.07)",
["--text"] = "#eef1e9",
["--muted"] = "rgba(238, 241, 233, 0.60)",
["--faint"] = "rgba(238, 241, 233, 0.34)",
["--danger"] = "#ff5a5a",
["--warn"] = "#ffd35a",
["--radius"] = "2px"
}
}
}
Lumo.Underwear = Lumo.Underwear or { male = 61, female = 15, texture = 0 }
-- ## NEW: Tattoo Categories with Camera Positions ##
-- This table now controls the UI categories and camera views.
-- Format: { "ZONE_ID", "Display Name", { camera_positions }, focus_point }
Lumo.TattooCats = {
{"ZONE_HEAD", "Head", {vec(0.0, 0.7, 0.7)}, vec(0.0, 0.0, 0.5)},
{"ZONE_TORSO", "Torso", {vec(0.0, 0.8, 0.2)}, vec(0.0, 0.0, 0.2)},
{"ZONE_BACK", "Back", {vec(0.0, -0.9, 0.2)}, vec(0.0, 0.0, 0.2)},
{"ZONE_LEFT_ARM", "Left Arm", {vec(-0.7, 0.0, 0.2)}, vec(-0.2, 0.0, 0.2)},
{"ZONE_RIGHT_ARM", "Right Arm", {vec(0.7, 0.0, 0.2)}, vec(0.2, 0.0, 0.2)},
{"ZONE_LEFT_LEG", "Left Leg", {vec(-0.2, 0.7, -0.7)}, vec(-0.2, 0.0, -0.6)},
{"ZONE_RIGHT_LEG", "Right Leg", {vec(0.2, 0.7, -0.7)}, vec(0.2, 0.0, -0.6)},
}
-- Keep only the defaults here. The tattoo list itself is gone.
Lumo.Services = {
defaults = {
barber_price = 500,
tattoo_price = 1500,
},
}
Lumo.CharacterCreation = {
enabled = true, -- master toggle for creation clothing flow
shopId = nil, -- set a specific clothing shopId or leave nil to auto-pick
allowedCategories = { -- categories shown during creation; empty = none
"jacket", "undershirt", "vests", "arms", "decals",
"neck", "bags", "pants", "shoes",
"hats", "mask", "glasses", "ear", "watch", "bracelet"
},
minItemsDefault = 200, -- min items per category (hard-capped to 200 in client)
maxItemsDefault = 200, -- max items per category (hard-capped to 200 in client)
minItemsPerCategory = {}, -- per-category min overrides
maxItemsPerCategory = {}, -- per-category max overrides
drawableAllowlist = {}, -- restrict to specific drawable IDs per category
drawableRanges = {}, -- restrict to drawable ranges; empty = all available drawables
forceFree = true -- set false to show prices during creation
}
-- Model selector for character creation.
-- If a player is not allowed by whitelist, their current model (from multicharacter) is kept.
Lumo.ModelSelection = {
enabled = true,
allowEveryone = false, -- true = all players can change model in creation UI
fallbackToCurrentModel = true, -- false = fallback to freemode model by gender
useDefaultPresets = false, -- true = auto add male/female freemode when presets list is empty
whitelist = {
citizenids = {}, -- example: { "ABCD1234" }
licenses = {
},
jobs = {}, -- example: { "police", "admin" }
gangs = {}, -- example: { "ballas" }
},
presets = {
{ id = "male_freemode", label = "Male Freemode", model = "mp_m_freemode_01", gender = "male" },
{ id = "female_freemode", label = "Female Freemode", model = "mp_f_freemode_01", gender = "female" },
{ id = "cop_male", label = "Police Male", model = "s_m_y_cop_01", gender = "male" },
{ id = "business_female", label = "Business Female", model = "a_f_y_business_01", gender = "female" },
{ id = "medic_male", label = "Medic Male", model = "s_m_m_paramedic_01", gender = "male" },
{ id = "hipster_female", label = "Hipster Female", model = "a_f_y_hipster_01", gender = "female" },
-- Only models listed here are shown in UI.
-- Add custom models below (gender should be "male", "female", or "keep")
-- { id = "cop_male", label = "Police Male", model = "s_m_y_cop_01", gender = "male" },
}
}
-- Job-only changing rooms. These use lumo-interact for the world prompt and
-- lumo-clothing for saved outfits; no second clothing resource is required.
Lumo.JobLockers = {
{ id = "police_mrpd", label = "Police Locker", job = "police", uniform = "police", coords = vector4(454.37, -993.23, 30.69, 357.75), ped = "s_m_y_cop_01", scenario = "WORLD_HUMAN_CLIPBOARD" },
{ id = "ems_pillbox", label = "EMS Locker", job = "ambulance", uniform = "ambulance", coords = vector4(341.93, -586.24, 43.32, 339.41), ped = "s_m_m_paramedic_01", scenario = "WORLD_HUMAN_CLIPBOARD" },
-- Each mechanic company gets a locker beside its existing parts stash.
{ id = "mechanic_city", label = "City Workshop Locker", job = "mechanic", uniform = "mechanic", coords = vector4(-345.60, -123.08, 39.01, 239.77), ped = "s_m_y_xmech_01", scenario = "WORLD_HUMAN_CLIPBOARD" },
{ id = "mechanic_harmony", label = "Harmony Workshop Locker", job = "mechanic2", uniform = "mechanic", coords = vector4(1186.15, 2636.62, 38.40, 283.56), ped = "s_m_y_xmech_01", scenario = "WORLD_HUMAN_CLIPBOARD" },
{ id = "mechanic_airport", label = "Airport Workshop Locker", job = "mechanic3", uniform = "mechanic", coords = vector4(-1139.92, -2005.77, 13.18, 142.70), ped = "s_m_y_xmech_01", scenario = "WORLD_HUMAN_CLIPBOARD" },
{ id = "mechanic_bennys", label = "Benny's Locker", job = "bennys", uniform = "mechanic", coords = vector4(-201.87, -1314.76, 31.09, 181.40), ped = "s_m_y_xmech_01", scenario = "WORLD_HUMAN_CLIPBOARD" },
{ id = "mechanic_beeker", label = "Beeker's Locker", job = "beeker", uniform = "mechanic", coords = vector4(99.92, 6621.45, 32.44, 139.65), ped = "s_m_y_xmech_01", scenario = "WORLD_HUMAN_CLIPBOARD" },
}
-- Presets are deliberately temporary: putting on a duty uniform does not
-- overwrite the player's saved civilian appearance. Saved outfits selected in
-- the Lumo wardrobe continue to use the normal persistent outfit flow.
Lumo.JobUniforms = {
police = {
male = {
{ label = "Patrol - Short Sleeve", minGrade = 0, outfitData = {
pants = { item = 24, texture = 0 }, arms = { item = 19, texture = 0 },
["t-shirt"] = { item = 58, texture = 0 }, vest = { item = 0, texture = 0 },
torso2 = { item = 55, texture = 0 }, shoes = { item = 51, texture = 0 },
accessory = { item = 0, texture = 0 }, bag = { item = 0, texture = 0 },
hat = { item = -1, texture = 0 }, glass = { item = 0, texture = 0 }, mask = { item = 0, texture = 0 },
} },
{ label = "Patrol - Long Sleeve", minGrade = 1, outfitData = {
pants = { item = 24, texture = 0 }, arms = { item = 20, texture = 0 },
["t-shirt"] = { item = 58, texture = 0 }, vest = { item = 0, texture = 0 },
torso2 = { item = 317, texture = 0 }, shoes = { item = 51, texture = 0 },
accessory = { item = 0, texture = 0 }, bag = { item = 0, texture = 0 },
hat = { item = -1, texture = 0 }, glass = { item = 0, texture = 0 }, mask = { item = 0, texture = 0 },
} },
},
female = {
{ label = "Patrol - Short Sleeve", minGrade = 0, outfitData = {
pants = { item = 133, texture = 0 }, arms = { item = 31, texture = 0 },
["t-shirt"] = { item = 35, texture = 0 }, vest = { item = 34, texture = 0 },
torso2 = { item = 48, texture = 0 }, shoes = { item = 52, texture = 0 },
accessory = { item = 0, texture = 0 }, bag = { item = 0, texture = 0 },
hat = { item = -1, texture = 0 }, glass = { item = 0, texture = 0 }, mask = { item = 0, texture = 0 },
} },
{ label = "Patrol - Long Sleeve", minGrade = 1, outfitData = {
pants = { item = 133, texture = 0 }, arms = { item = 31, texture = 0 },
["t-shirt"] = { item = 35, texture = 0 }, vest = { item = 34, texture = 0 },
torso2 = { item = 327, texture = 0 }, shoes = { item = 52, texture = 0 },
accessory = { item = 0, texture = 0 }, bag = { item = 0, texture = 0 },
hat = { item = -1, texture = 0 }, glass = { item = 0, texture = 0 }, mask = { item = 0, texture = 0 },
} },
},
},
ambulance = {
male = {
{ label = "Paramedic Duty Uniform", minGrade = 0, outfitData = {
arms = { item = 85, texture = 0 }, ["t-shirt"] = { item = 129, texture = 0 },
torso2 = { item = 250, texture = 0 }, vest = { item = 0, texture = 0 },
decals = { item = 58, texture = 0 }, accessory = { item = 127, texture = 0 },
bag = { item = 0, texture = 0 }, pants = { item = 96, texture = 0 },
shoes = { item = 54, texture = 0 }, mask = { item = 0, texture = 0 },
hat = { item = -1, texture = 0 }, glass = { item = 0, texture = 0 },
} },
},
female = {
{ label = "Paramedic Duty Uniform", minGrade = 0, outfitData = {
arms = { item = 109, texture = 0 }, ["t-shirt"] = { item = 159, texture = 0 },
torso2 = { item = 258, texture = 0 }, vest = { item = 0, texture = 0 },
decals = { item = 66, texture = 0 }, accessory = { item = 97, texture = 0 },
bag = { item = 0, texture = 0 }, pants = { item = 99, texture = 0 },
shoes = { item = 55, texture = 0 }, mask = { item = 0, texture = 0 },
hat = { item = -1, texture = 0 }, glass = { item = 0, texture = 0 },
} },
},
},
mechanic = {
male = {
{ label = "Workshop Coveralls", minGrade = 0, outfitData = {
mask = { item = 0, texture = 0 }, arms = { item = 112, texture = 0 },
pants = { item = 98, texture = 0 }, shoes = { item = 24, texture = 0 },
["t-shirt"] = { item = 15, texture = 0 }, torso2 = { item = 104, texture = 0 },
vest = { item = 0, texture = 0 }, hat = { item = -1, texture = 0 },
} },
},
female = {
{ label = "Workshop Coveralls", minGrade = 0, outfitData = {
mask = { item = 0, texture = 0 }, arms = { item = 113, texture = 0 },
pants = { item = 101, texture = 0 }, shoes = { item = 25, texture = 0 },
["t-shirt"] = { item = 14, texture = 0 }, torso2 = { item = 109, texture = 0 },
vest = { item = 0, texture = 0 }, hat = { item = -1, texture = 0 },
} },
},
},
}
-- "None" preset values used when a category item with drawable -1 is applied.
-- Props use -1 to clear. Clothing components use explicit fallback drawables.
Lumo.NoneComponents = {
male = {
["torso2"] = { item = 15, texture = 0 },
["t-shirt"] = { item = 15, texture = 0 },
vest = { item = 0, texture = 0 },
arms = { item = 15, texture = 0 },
pants = { item = 21, texture = 0 },
shoes = { item = 34, texture = 0 },
mask = { item = 0, texture = 0 },
bag = { item = 0, texture = 0 },
accessory = { item = 0, texture = 0 },
decals = { item = 0, texture = 0 },
},
female = {
["torso2"] = { item = 15, texture = 0 },
["t-shirt"] = { item = 14, texture = 0 },
vest = { item = 0, texture = 0 },
arms = { item = 15, texture = 0 },
pants = { item = 15, texture = 0 },
shoes = { item = 35, texture = 0 },
mask = { item = 0, texture = 0 },
bag = { item = 0, texture = 0 },
accessory = { item = 0, texture = 0 },
decals = { item = 0, texture = 0 },
}
}bridge/config.lua
lua
--- lumo-clothing — framework settings
---
--- QBCore, Qbox and ESX are detected automatically; with none of them running
--- the script runs standalone. Most servers never touch this file. The rest
--- of the settings live in shared.lua.
---
--- This folder (bridge/) is the script's built-in framework layer. You do NOT
--- need the separate lumo-bridge resource.
LumoBridgeConfig = {
--- 'auto' | 'qb' | 'qbx' | 'esx' | 'standalone' | 'custom'
--- auto — Qbox (qbx_core) > QBCore (qb-core) > ESX (es_extended) > Fallback
Framework = 'auto',
--- Used by 'auto' when no core is running. 'standalone' = no framework.
--- Set 'custom' only if you wire your own core in Custom below.
Fallback = 'standalone',
--- Print the detected framework and every unresolved call to the console.
Debug = false,
--- Resource names, in case yours are renamed.
Resources = {
qb = 'qb-core',
qbx = 'qbx_core',
esx = 'es_extended',
oxlib = 'ox_lib',
},
--- How long a server callback waits for its answer before giving up (ms).
CallbackTimeout = 15000,
--- Inventory for clothing items (Config.ClothingItems in shared.lua).
--- 'auto' probes: lumo-inventory, ox_inventory, qb-inventory, qs-inventory,
--- ps-inventory, then ESX's own inventory. Force one if you run several.
Inventory = 'auto',
--- Targeting for shop peds and lockers. 'auto' probes: ox_target,
--- qb-target, qtarget; without one, an [E] prompt is used.
Target = 'auto',
ESX = {
--- How ESX multichar identifiers look. %s = slot, %l = licence hex.
IdentifierFormat = 'char%s:%l',
--- true only for an ESX build without per-slot identifiers.
SingleCharacter = false,
--- This script is not a character selector; leave false.
VerifyMulticharAtBoot = false,
},
--- Standalone (no framework). A character script that owns characters
--- (lumo-multicharacter) is used when it runs; without one every player is
--- one character keyed by their Rockstar licence.
Standalone = {
OwnsCharacters = false, -- keep false in this script
CharacterResource = 'lumo-multicharacter',
RememberPosition = false, -- positions are a spawn script's job
SetPedModel = false, -- the saved skin decides the ped
--- With neither a character script nor lumo-spawn, load the saved
--- skin when FiveM's spawnmanager spawns the player.
AnnounceOnSpawn = true,
--- There is no money in standalone. With FreeShopping on, shops,
--- barbers and tattoos cost nothing. To charge, plug your economy in:
--- GetMoney = function(source, account) return number end,
--- RemoveMoney = function(source, account, amount, reason) return true/false end,
--- AddMoney = function(source, account, amount, reason) return true/false end,
--- account is 'cash' or 'bank'. Any hook set here wins over FreeShopping.
FreeShopping = true,
Money = {},
--- Job lockers need a job. Return { name = 'police', grade = { level = 2 } }
--- from here if your server tracks jobs, or leave nil (no job lockers).
GetJob = nil,
},
--- Only for Framework = 'custom'. See docs/CUSTOM-FRAMEWORKS.md.
Custom = {
client = {},
server = {},
inventory = {},
},
}