Documentação

Documentação do Interact system

Sistema de interação 3D + engine de diálogo de NPC, sem nenhuma dependência. Coloque uma interação em qualquer lugar do mapa: numa entity, numa coordenada qualquer, ou num NPC que você spawna na mesma chamada. Sem ox_lib, sem framework, nada para configurar do lado do servidor.

  • ESX
  • QBCore
  • ox
  • vRP
  • Standalone
01

Instalação

  1. Coloque o resource

    Baixe o script pela sua conta Tebex e copie a pasta avenida_interact/ para resources/[avenida]/ no seu servidor.

  2. Ative no server.cfg

    Adicione ensure avenida_interact. Nada precisa subir antes: desde a 1.1.0 o resource não tem dependência, então a ordem de carregamento não importa.

  3. Defina seus padrões

    Abra config/config.lua e ajuste o que vale para todos os pontos: rótulo, tecla, distâncias showIcon e canInteract, ícone, cor, cores nomeadas, distância de spawn dos peds, e Config.Hud para escolher qual HUD some durante um diálogo.

  4. Apague as demos

    Os exemplos em config/entities/, config/peds/ e config/positions/ ficam ativos enquanto os arquivos estiverem lá. Apague os que não quiser, o manifest varre essas pastas, não tem mais nada para editar.

  5. Teste o hot reload

    Suba o servidor e digite /restart avenida_interact no jogo. Os ícones somem e voltam limpos, e os pontos registrados pelos seus próprios resources sobrevivem, sem você precisar reiniciar a sua sessão.

02

Configuração

Seu primeiro ponto de interação em 30 segundos

Crie um myscript/client.lua no seu próprio resource e cole isto. Um marcador ciano aparece a 30m, o prompt "E - Hello" abre a 2m. A única coisa que precisa estar rodando é o próprio avenida_interact.

lua
-- myscript/client.lua
exports.avenida_interact:positionRegister({
    coords      = vec3(-265.0, -963.6, 31.2),
    showIcon    = 30.0,           -- marker fades in at 30m
    canInteract = 2.0,            -- label unrolls at 2m
    hintIcon    = 'interact',     -- see the icon list below
    hintColor   = '#22d3ee',      -- hex, or an alias from Config.Colors
    message     = 'Hello',
    bind        = 'E',
    onInteract  = function()
        TriggerEvent('chat:addMessage', { args = { 'Hello, world!' } })
    end,
})
03

Exports

Interact Points

  • positionRegister
    exports.avenida_interact:positionRegister(cfg) --> id

    Registra um ponto em coordenadas fixas. Aceita todos os campos visuais (hintIcon, hintColor, showIcon, canInteract, offsetZ, blips) mais choices[] para várias teclas no mesmo ponto. canInteract aceita um número ou uma função, então você pode travar o prompt num emprego, num item ou num cooldown. Retorna um id.

  • pedRegister
    exports.avenida_interact:pedRegister(cfg) --> id

    Spawna um NPC configurado (modelo, animDict e animName ou um scenario, blip), pendura um ponto nele e retorna o índice. O ped é invencível, não foge e não cai em ragdoll. Ele nasce e some no raio de Config.PedSpawnRange, 50m por padrão. Coloque randomComponents = false num vendedor com nome, senão cada jogador vê uma roupa diferente.

  • pedRemove
    exports.avenida_interact:pedRemove(idx)

    Adicionado na 1.1.0. Despawna um ped registrado com pedRegister e apaga o registro dele, então um NPC de missão temporário pode ir embora quando a missão acaba.

  • entityRegister
    exports.avenida_interact:entityRegister(cfg) --> id

    Pendura um ponto numa entity existente, passada em cfg.entity. Suporta cfg.bone para prender o prompt num bone em vez do centro da entity. Serve para o que outro script já spawnou: um veículo, um prop colocado, um item largado.

  • entityRegisterByHash
    exports.avenida_interact:entityRegisterByHash(hash, cfg)

    Watcher passivo: a config vale para toda entity daquele modelo, as já carregadas e as que aparecerem depois. Uma única thread cobre todos os hashes registrados e faz uma varredura a cada 5s. É isso que você quer para caixas eletrônicos, máquinas de venda e portas, em vez de ficar mantendo uma lista de coordenadas.

  • entityRemove
    exports.avenida_interact:entityRemove(idx)

    Solta o ponto que o entityRegister devolveu. A entity em si não é tocada.

  • interactCreate
    exports.avenida_interact:interactCreate(cfg) --> id

    Ponto em runtime, criado e removido sob demanda. Use quando o ponto só existe por um tempo: um objetivo de missão, uma mochila no chão, um corpo para revistar.

  • interactRemove
    exports.avenida_interact:interactRemove(id)

    Remove um ponto criado por interactCreate, junto com o blip dele se ele tiver declarado um.

  • unlockLastInteract
    exports.avenida_interact:unlockLastInteract()

    Um ponto se tranca depois de usado, para que uma tecla segurada não dispare duas vezes. Chame isto no fim da sua ação para rearmá-lo. Se o jogador ainda estiver perto, o prompt volta na hora, senão ele espera o jogador chegar perto de novo.

Prompt API

  • HandleTextUI
    exports.avenida_interact:HandleTextUI(id, data) --> duiHandler

    Cria ou atualiza uma bolha 3D autônoma, sem nenhum ponto de interação por trás. Para quando você quer controlar a visibilidade pela sua própria thread. Retorna o handler que você passa para o Draw3DSprite.

  • Draw3DSprite
    exports.avenida_interact:Draw3DSprite({ duiHandler, coords, maxDistance, hintOnly? })

    Desenha a bolha no mundo a partir de uma thread de render, corrigida conforme a proporção da tela. Com hintOnly = true só o marcador é desenhado, que é o estado de longa distância de um ponto normal.

  • CloseTextUI
    exports.avenida_interact:CloseTextUI(id)

    Esconde uma bolha, mas mantém o DUI dela na memória, então reabrir é instantâneo.

  • RemoveTextUI
    exports.avenida_interact:RemoveTextUI(id)

    Remove uma bolha de vez e libera o DUI dela. Use quando souber que ela não vai voltar.

  • RemoveTextUIs
    exports.avenida_interact:RemoveTextUIs()

    Remove todas as bolhas ativas. O resource chama isso sozinho quando ele para.

  • IsDuiVisible
    exports.avenida_interact:IsDuiVisible() --> boolean

    Verdadeiro enquanto tem um prompt na tela. Leia isso para os seus próprios prompts não empilharem em cima de uma interação.

Key handler

  • HandleHoldTextUI
    exports.avenida_interact:HandleHoldTextUI(id, data) --> duiHandler

    A camada em que os pontos são construídos: ela desenha um prompt em Coords e fica de olho na tecla dele numa thread manager a 50ms. data recebe BindToHold (índice de controle), DistanceHold, Coords, ChoicesUI e ChoicesCtrl para um prompt de várias teclas, onCallback(id) e canInteract(id, dist). Dispara ao apertar, com debounce de 500ms, e o seu callback roda num pcall, então um erro do seu lado não quebra o prompt.

  • CloseHoldTextUI
    exports.avenida_interact:CloseHoldTextUI(id)

    Esconde o prompt sem destruir a instância, para uma saída limpa quando o jogador sai da zona.

  • RemoveHoldTextUI
    exports.avenida_interact:RemoveHoldTextUI(id)

    Destrói uma instância e libera o DUI dela.

  • RemoveHoldTextUIs
    exports.avenida_interact:RemoveHoldTextUIs()

    Destrói todas as instâncias ativas.

04

Receitas

Posição fixa com blip no mapa

O caso mais simples: um ponto em coordenadas, com o blip declarado na mesma table. O marcador aparece a 30m, o rótulo a 2m. Cole isso em qualquer arquivo de client do seu resource.

lua
exports.avenida_interact:positionRegister({
    coords      = vec3(149.5, -1040.4, 29.4),
    showIcon    = 30.0,
    canInteract = 2.0,
    hintIcon    = 'bank',
    hintColor   = 'blue',
    message     = 'Use the ATM',
    bind        = 'E',
    onInteract  = function()
        TriggerEvent('myserver-banking:openATM')
    end,
    blips = {
        sprite  = 277,
        color   = 2,
        scale   = 0.8,
        text    = 'ATM',
        display = 4,
    },
})

Todos os caixas eletrônicos do mapa, duas teclas cada

O watcher registra a sua config uma vez por modelo e aplica em todo prop daquele modelo, já carregado ou que apareça depois. Cada escolha leva rótulo, cor e condition próprios, então o roubo só aparece quando o seu script de polícia deixa. Atenção ao nome do campo: uma escolha usa condition, não canInteract.

lua
local ATM_PROPS = {
    'prop_atm_01', 'prop_atm_02', 'prop_atm_03', 'prop_fleeca_atm',
}

for _, prop in ipairs(ATM_PROPS) do
    exports.avenida_interact:entityRegisterByHash(prop, {
        canInteract = 1.2,
        offsetZ     = 1.0,
        hintIcon    = 'bank',
        hintColor   = 'blue',
        choices = {
            {
                bind       = 'E',
                message    = 'Use the ATM',
                onInteract = function()
                    TriggerEvent('myserver-banking:atmUse')
                end,
            },
            {
                bind      = 'G',
                message   = 'Rob the ATM',
                hintColor = 'red',
                condition = function()
                    return exports['myserver-police']:hasEnoughCops(2)
                end,
                onInteract = function(entity)
                    TriggerServerEvent('myserver-robbery:startATM', NetworkGetNetworkIdFromEntity(entity))
                end,
            },
        },
    })
end

NPC com diálogo ramificado e waypoint no GPS

Um NPC de missão completo: o ped nasce com a animação e o blip dele, e as respostas do diálogo executam o que você quiser, aqui o native SetNewWaypoint. changeDialog troca o texto e as respostas, então dá para aninhar tão fundo quanto a conversa pedir. randomComponents = false mantém a roupa dela igual para todos os jogadores.

lua
exports.avenida_interact:pedRegister({
    coords           = vec4(-1393.81, -1064.09, 3.17, 266.09),
    model            = 'a_f_y_vinewood_04',
    animDict         = 'anim@amb@waving@male',
    animName         = 'ground_wave',
    randomComponents = false,
    message          = 'Talk with Maria',
    showIcon         = 10.0,
    canInteract      = 3.0,
    hintIcon         = 'talk',
    hintColor        = 'gold',
    blips = { sprite = 119, color = 50, text = 'Lumberjack', scale = 0.8 },
    dialogue = {
        name     = 'Maria',
        startMsg = 'Hello, are you ready to start your shift?',
        elements = {
            {
                label  = 'Yes, where do I start?',
                action = function(changeDialog, close)
                    SetNewWaypoint(-584.01, 5491.59)
                    changeDialog('The forest is on your GPS now.', {
                        { label = 'Got it!', action = function(_, close) close() end },
                    })
                end,
            },
            {
                label  = 'I\'ll come back later',
                action = function(_, close) close() end,
            },
        },
    },
})
05

Perguntas

Eu realmente não preciso do ox_lib?

Não precisa. Desde a 1.1.0 o bridges/compat.lua implementa a criação de DUI, os pontos de proximidade e os helpers de espera em natives puros, e a linha @ox_lib/init.lua está comentada no manifest. Se preferir passar pelo ox_lib, coloque Config.UseOxLib = true e descomente essa linha. Os dois caminhos são mantidos, o nativo é o padrão.

Meu marcador não aparece, onde eu olho?

Primeiro o alcance: showIcon vale 4m por padrão em Config.Defaults, o que é curto se você esperava um marcador visível de longe, então passe o seu próprio valor. Segundo, com entityRegister a entity precisa existir na hora da chamada, enquanto entityRegisterByHash espera por ela, e é isso que você quer para props do jogo. Terceiro, coloque Config.Debug = true para ver os erros internos no console do client.

Minha condição de escolha é ignorada

Uma escolha lê condition, não canInteract. canInteract é o campo do ponto em si, e lá ele aceita um número ou uma função. Dentro de choices[] a trava se chama condition = function() ... end, e uma escolha cuja condição retorna false simplesmente não é desenhada.

Que cores eu posso usar?

Qualquer hex como #22d3ee, ou um nome de Config.Colors, que vem com red, blue, green, gold, purple e white. Essa table está fora do escrow, coloque a paleta do seu servidor nela e use os seus próprios nomes em tudo. Um hintIcon desconhecido é desenhado como texto, então um emoji quebra o galho como ícone.

O prompt ficava esticado no meu ultrawide

Isso foi corrigido na 1.1.0. A textura DUI agora está travada em 1920x1080 e é corrigida na hora de desenhar conforme a proporção real da tela, e o ícone não muda mais de tamanho com a distância. As caixas de resposta dos diálogos também ganharam altura acima de 2560x1440. Atualize o resource se você ainda estiver na 1.0.x.

Meus pontos continuam visíveis depois que meu resource para

A posse é rastreada com GetInvokingResource(), então um ponto pertence ao resource que chamou o export. Se você registrar a partir de um handler compartilhado que roda no contexto de outro resource, a limpeza segue esse outro resource. Coloque a chamada dentro de uma função do resource dono do ponto. Desde a 1.1.0 os pontos também são apagados quando um resource inicia, então um restart não deixa mais fantasmas.

Como eu ligo isso no ESX, QBCore, Qbox, ox ou vRP?

Você não precisa ligar em nada: o resource não conhece o seu framework e nunca fala com o seu servidor. Leia o emprego ou o grade do jogador dentro de canInteract ou da condition de uma escolha, e chame os seus próprios events pelo onInteract. Tudo que roda no seu framework roda atrás dessas duas funções, sem precisar mudar nada.

Quantos pontos eu posso registrar?

Medido em 0.00ms em repouso com 100 pontos. O desenho só acontece dentro do alcance showIcon, e a checagem das teclas roda a 50ms para as instâncias que estão perto, então milhares de pontos espalhados pelo mapa não custam nada enquanto não estiverem todos empilhados na mesma rua.

Carrinho
Spirit RP
Discord