Skip to content

Target System

ox_target / qtarget replacement — detects aimed entity or zone and displays interaction menu. Supports entities, models, spheres, boxes and polygons.

5 min read

How it works

A background thread runs at 10 Hz (100ms). When the player aims at an entity or enters a registered zone:

  1. A passive reticle appears in the center of the screen.
  2. Player presses the target key (default: Left Ctrl / INPUT_DUCK) → the action menu opens.
  3. Selecting an action triggers the callback; Escape or ✕ button closes the menu.

When no registrations exist, the thread sleeps at 1 Hz (no CPU cost).


Registration types

Entity — specific handle

local id = exports.LastMenu:target_add_entity(entity, opts)
FieldTypeDefaultDescription
entitynumberEntity handle
opts.labelstring"Interact"Action menu header label
opts.iconstring"eye"Lucide icon in reticle
opts.distancenumber3.0Max distance in meters
opts.actionstable{}Array of action objects (or use the builder API)
opts.on_enterfunctionnilCalled once when player enters zone
opts.on_leavefunctionnilCalled once when player leaves zone

Model — all matching entities

local id = exports.LastMenu:target_add_model(model, opts)
FieldTypeDescription
modelstring\|number\|nilModel name, hash, or nil for all vehicles

Other fields are identical to target_add_entity.


Sphere zone

local id = exports.LastMenu:target_add_sphere(coords, radius, opts)
FieldTypeDefault
coordsvector3
radiusnumber2.0

Rectangular zone (box)

Axis-aligned rectangular zone, rotatable by heading.

local id = exports.LastMenu:target_add_box(coords, opts)
FieldTypeDefaultDescription
opts.widthnumber2.0Total width on local X axis
opts.lengthnumber2.0Total length on local Y axis
opts.headingnumber0.0Rotation in degrees (GTA: clockwise from North)

width and length are not available through the builder API. Use the table form when you need specific dimensions.


Polygonal zone

2D polygon with optional Z bounds.

local id = exports.LastMenu:target_add_poly(points, opts)
FieldTypeDefaultDescription
pointsvector2[]\|vector3[]Vertices defining the polygon
opts.minZnumber-math.hugeLower Z bound
opts.maxZnumbermath.hugeUpper Z bound

minZ / maxZ are not available through the builder API. Use the table form when you need Z constraints.

Example — L-shaped floor plan:

local storeFloor = {
    vector2(312.0, -780.0),
    vector2(320.0, -780.0),
    vector2(320.0, -795.0),
    vector2(328.0, -795.0),
    vector2(328.0, -808.0),
    vector2(312.0, -808.0),
}

local storeId = exports.LastMenu:target_add_poly(storeFloor, {
    minZ     = 28.5,
    maxZ     = 32.0,
    label    = "24/7 Store",
    icon     = "store",
    on_enter = function()
        exports.LastMenu:notify(function(n) n:message("Welcome!"); n:type("info") end)
    end,
    on_leave = function()
        exports.LastMenu:notify(function(n) n:message("Thanks for visiting."); n:type("info") end)
    end,
    actions  = {
        { label = "Shop",        icon = "shopping-cart", cb = function() end },
        { label = "Rob cashier", icon = "alert-triangle",
          condition = function() return IsPedArmed(PlayerPedId(), 6) end,
          cb = function() TriggerServerEvent("crime:robbery") end },
    },
})

AddEventHandler("onResourceStop", function(res)
    if res ~= GetCurrentResourceName() then return end
    exports.LastMenu:target_remove(storeId)
end)

Action object

Each entry in opts.actions supports:

FieldTypeDescription
labelstringButton text
iconstringLucide icon
colorstringAccent color (hex) applied to icon and optional gradient
gradientboolGradient background on the action button
badgestringSmall text badge on the right
cbfunction(entity)Called on click. entity = entity handle (or nil for zones)
conditionbool\|function(entity)→boolHide the action when false (preferred alias for visible)
visiblebool\|function(entity)→boolHide the action when false
disabledbool\|function(entity)→boolShow grayed-out, no callback
confirm_holdnumber\|trueHold-to-confirm duration in ms
cooldownnumberRecharge delay in ms after click
persist_keystringStable localStorage key for cooldown persistence
keep_openboolKeep the target menu open after callback fires

condition takes priority over visible when both are set.

actions = {
    {
        label   = "Search body",
        icon    = "search",
        visible = function(ent) return IsPedDeadOrDying(ent, true) end,
        cb      = function(ent) print("Searched ped " .. ent) end,
    },
    {
        label    = "Heal",
        icon     = "plus-circle",
        disabled = function(ent) return GetEntityHealth(ent) >= 200 end,
        cb       = function(ent) print("Healed ped " .. ent) end,
    },
}

Builder API

The opts parameter accepts either a table (classic) or a builder function (recommended). The builder is auto-detected — same pattern as LM:context().

LM:target_add_model("mp_m_freemode_01", function(t)
    t:label("Player")
    t:icon("user")
    t:distance(3.0)
    t:button("Interact", { icon = "hand", cb = function(entity) end })
end)

Header methods

MethodTypeDefaultDescription
t:label(str)string"Interact"Title of the action menu
t:icon(str)string"eye"Lucide icon shown in the reticle
t:distance(num)number3.0Max distance in metres
t:banner(url)stringnilImage/GIF URL displayed at the top of the menu
t:on_enter(fn)function()nilCalled once when the player enters the zone
t:on_leave(fn)function()nilCalled once when the player leaves the zone

t:button(label, opts)

Simple action. Callback receives entity (handle, or nil for zone-based targets).

t:button("Repair", {
    icon         = "wrench",
    gradient     = true,
    confirm_hold = 2000,
    cooldown     = 60000,
    condition    = function() return isOnDuty end,
    cb           = function(entity) repairVehicle(entity) end,
})

t:toggle(label, opts)

Persistent ON/OFF button. Callback receives entity, boolean_value.

t:toggle("Lock", {
    icon    = "lock",
    default = false,
    cb      = function(entity, val)
        SetVehicleDoorsLocked(entity, val and 2 or 1)
    end,
})

t:checkbox(label, opts)

Same interface as toggle but different visual rendering.

t:slider(label, opts)

Numeric slider. Callback receives entity, numeric_value.

t:slider("Fuel", {
    icon    = "fuel",
    min     = 0,
    max     = 100,
    step    = 5,
    default = 50,
    suffix  = "%",
    cb      = function(entity, val)
        -- SetVehicleFuelLevel(entity, val * 0.65)
    end,
})

t:separator()

Visual divider between action groups.

t:group(label, opts, fn)

Groups actions inside a collapsible accordion. All action types are available inside a group.

t:group("Services", { icon = "tool" }, function(g)
    g:button("Clean",          { icon = "droplets",         cb = cleanVehicle  })
    g:button("Inflate tyres",  { icon = "circle-dot",       cb = inflateTyres  })
    g:button("Charge battery", { icon = "battery-charging", cb = chargeBattery })
    -- g:toggle / g:checkbox / g:slider also available
end)
OptionTypeDescription
iconstringIcon for the group header
openboolExpanded by default (false)

t:submenu(label, opts)

Arrow button that calls a function without closing the target menu.

t:submenu("Mechanic menu", {
    icon = "hard-hat",
    cb   = function(entity) openMechanicRadial(entity) end,
})

The target menu stays open. Use LM:context() or LM:radial() inside the callback for a sub-level of actions.

Difference from button: a button closes the target menu before firing its callback. A submenu keeps it open.

Method availability

Methodt: (root)g: (group)
button
toggle
checkbox
slider
separator
group
submenu
label / icon / distance / banner
on_enter / on_leave

Removal

-- Remove one registration
exports.LastMenu:target_remove(id)

-- Remove all registrations
exports.LastMenu:target_clear()

Always clean up on resource stop:

AddEventHandler("onResourceStop", function(res)
    if res == GetCurrentResourceName() then
        exports.LastMenu:target_remove(myId)
    end
end)

Registrations are also automatically removed when the resource that created them stops.


Registration merging

When multiple registrations match simultaneously, their actions are merged into a single menu. Header label priority: entity > model > zone.


Visual debug

Enable Config.debugTarget = true in client/config.lua to visualize all zones in-game:

Zone typeVisualization
sphereBlue translucent sphere
boxMarker + corner lines
polyOutline connecting all vertices

The debug thread sleeps at 500ms when disabled — no production cost.


Full example

local LM = exports.LastMenu

LM:target_add_model(nil, function(t)  -- nil = all vehicles
    t:label("Vehicle")
    t:icon("car")
    t:distance(4.0)

    t:button("Repair", {
        icon         = "wrench",
        gradient     = true,
        confirm_hold = 2000,
        cooldown     = 60000,
        condition    = function() return isOnDuty end,
        cb           = function(entity) repairVehicle(entity) end,
    })

    t:button("Inspect / Estimate", {
        icon = "clipboard-list",
        cb   = function(entity) openDiagnostic(entity) end,
    })

    t:separator()

    t:group("Services", { icon = "tool" }, function(g)
        g:button("Clean", {
            icon      = "droplets",
            condition = function() return isOnDuty end,
            cb        = function(entity) cleanVehicle(entity) end,
        })
        g:button("Inflate tyres", {
            icon = "circle-dot",
            cb   = function(entity) inflateAllTyres(entity) end,
        })
    end)

    t:group("Options", { icon = "settings-2" }, function(g)
        g:toggle("Lock", {
            icon    = "lock",
            default = false,
            cb      = function(entity, val)
                SetVehicleDoorsLocked(entity, val and 2 or 1)
            end,
        })
        g:slider("Fuel", {
            icon = "fuel", min = 0, max = 100, step = 5, suffix = "%",
            cb   = function(entity, val) end,
        })
    end)

    t:submenu("Mechanic menu", {
        icon = "hard-hat",
        cb   = function(entity) openMechanicRadial(entity) end,
    })
end)