> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fivenetwork.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Instalação

> Passo a passo para instalar e configurar o Five Pets no seu servidor FiveM.

## Dependências

O Five Pets possui **apenas uma dependência obrigatória**. A integração com o seu framework é feita através de um arquivo de adaptação separado — não existe acoplamento direto com nenhum framework específico.

<CardGroup cols={2}>
  <Card title="oxmysql" icon="database">
    Única dependência obrigatória. Responsável por toda a comunicação com o banco de dados MySQL/MariaDB.
  </Card>

  <Card title="Qualquer framework" icon="puzzle-piece">
    ESX, QBCore, vRP, ox\_core ou customizado. A integração é feita adaptando `config/functions.lua`.
  </Card>
</CardGroup>

<Info>
  O sistema de notificações detecta automaticamente o **ox\_lib** e o utiliza se disponível. Caso contrário, usa o evento de notificação padrão do servidor.
</Info>

***

## Banco de Dados

As tabelas são criadas automaticamente na primeira inicialização do recurso. As estruturas abaixo servem apenas como referência.

<Steps>
  <Step title="Tabela de instâncias">
    Armazena cada pelúcia individualmente com seu nível, XP, benefícios e status de equip.

    ```sql theme={null}
    CREATE TABLE IF NOT EXISTS `core_pets_instances` (
      `id`            INT          NOT NULL AUTO_INCREMENT,
      `user_id`       INT          NOT NULL,
      `item_key`      VARCHAR(100) NOT NULL,
      `level`         INT          NOT NULL DEFAULT 1,
      `xp`            INT          NOT NULL DEFAULT 0,
      `benefits_json` TEXT,
      `equipped`      TINYINT      NOT NULL DEFAULT 0,
      `created_at`    TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`),
      INDEX `idx_user_id`         (`user_id`),
      UNIQUE KEY `unique_user_item` (`user_id`, `item_key`),
      INDEX `idx_user_equipped`   (`user_id`, `equipped`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    ```
  </Step>

  <Step title="Tabela de pelúcia em destaque">
    Armazena a pelúcia marcada como destaque por cada jogador.

    ```sql theme={null}
    CREATE TABLE IF NOT EXISTS `core_pets_featured` (
      `user_id`  INT          NOT NULL,
      `item_key` VARCHAR(100) DEFAULT NULL,
      PRIMARY KEY (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    ```
  </Step>

  <Step title="Tabela de propostas de troca">
    Armazena propostas de troca de forma assíncrona — o destinatário não precisa estar online no momento da proposta. O campo `status` percorre os valores `pending`, `accepted`, `declined`, `cancelled` e `failed`.

    ```sql theme={null}
    CREATE TABLE IF NOT EXISTS `core_pets_trades` (
      `id`           INT          NOT NULL AUTO_INCREMENT,
      `from_user_id` INT          NOT NULL,
      `to_user_id`   INT          NOT NULL,
      `offer_json`   TEXT,
      `request_json` TEXT,
      `status`       VARCHAR(20)  NOT NULL DEFAULT 'pending',
      `message`      VARCHAR(200),
      `created_at`   TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `resolved_at`  TIMESTAMP    NULL,
      PRIMARY KEY (`id`),
      INDEX `idx_to_user_status`   (`to_user_id`, `status`),
      INDEX `idx_from_user_status` (`from_user_id`, `status`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    ```
  </Step>

  <Step title="Tabela legada (compatibilidade)">
    Mantida apenas para que a migração das coleções antigas para o novo formato de instâncias funcione. Não é mais escrita pelo recurso.

    ```sql theme={null}
    CREATE TABLE IF NOT EXISTS `core_pets_collections` (
      `id`       INT          NOT NULL AUTO_INCREMENT,
      `user_id`  INT          NOT NULL,
      `item_key` VARCHAR(100) NOT NULL,
      `qty`      INT          NOT NULL DEFAULT 0,
      PRIMARY KEY (`id`),
      UNIQUE KEY `unique_user_item` (`user_id`, `item_key`),
      INDEX `idx_user_id` (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    ```
  </Step>
</Steps>

***

## Instalação do Recurso

<Steps>
  <Step title="Copie a pasta do recurso">
    Coloque a pasta `core-pets` dentro do diretório de recursos do seu servidor (ex: `resources/[core]/`).
  </Step>

  <Step title="Informe o token de licença">
    Abra o arquivo `token.lua` na raiz do recurso e substitua `SEU-TOKEN-AQUI` pelo token recebido.

    ```lua token.lua theme={null}
    exports('license', function ()
        return 'SEU-TOKEN-AQUI'
    end)
    ```
  </Step>

  <Step title="Adapte o config/functions.lua">
    Arquivo único de integração com o seu framework. Veja a seção [Adaptação de Framework](#adaptação-de-framework) abaixo.
  </Step>

  <Step title="Configure o recurso">
    Edite os arquivos em `config/` conforme descrito na seção [Configuração](#configuração).
  </Step>

  <Step title="Adicione à server.cfg">
    Certifique-se de iniciar o recurso **após** o oxmysql e o seu framework.

    ```text theme={null}
    ensure oxmysql
    ensure seu-framework
    ensure core-pets
    ```
  </Step>
</Steps>

***

## Adaptação de Framework

Toda a comunicação com o framework está isolada em `config/functions.lua`. Para integrar com o framework do seu servidor, implemente as seguintes funções nesse arquivo:

<AccordionGroup>
  <Accordion title="Identificação do jogador" icon="user">
    O script precisa de um identificador numérico único por jogador para associar dados no banco.

    ```lua theme={null}
    -- Retorna o ID único do jogador a partir do source (server ID)
    function Functions.getUserId(source)
        -- Exemplo vRP (Creative Enchanted):
        return vRP.Passport(source)

        -- Exemplo vRPex / vRP padrão:
        -- return vRP.getUserId(source)

        -- Exemplo ESX:
        -- local xPlayer = ESX.GetPlayerFromId(source)
        -- return xPlayer and xPlayer.getIdentifier() or nil

        -- Exemplo QBCore:
        -- local Player = QBCore.Functions.GetPlayer(source)
        -- return Player and Player.PlayerData.citizenid or nil

        -- Exemplo standalone (usa o próprio source):
        -- return source
    end

    -- Retorna o source do jogador a partir do ID único
    function Functions.getUserSource(userId)
        -- Exemplo vRP (Creative Enchanted):
        return vRP.Source(userId)

        -- Exemplo vRPex / vRP padrão:
        -- return vRP.getUserSource(userId)

        -- Exemplo ESX:
        -- for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do
        --     if xPlayer.getIdentifier() == userId then return xPlayer.source end
        -- end

        -- Exemplo QBCore:
        -- for _, src in ipairs(QBCore.Functions.GetPlayers()) do
        --     local Player = QBCore.Functions.GetPlayer(src)
        --     if Player and Player.PlayerData.citizenid == userId then return src end
        -- end
    end

    -- Retorna o nome completo do jogador
    function Functions.getUserName(userId)
        -- Exemplo vRP (Creative Enchanted):
        return vRP.FullName(userId)

        -- Exemplo vRPex / vRP padrão:
        -- local identity = vRP.getUserIdentity(userId)
        -- return identity and (identity.firstname .. " " .. identity.name) or "Desconhecido"

        -- Exemplo ESX:
        -- local xPlayer = ESX.GetPlayerFromIdentifier(userId)
        -- return xPlayer and xPlayer.getName() or "Desconhecido"

        -- Exemplo QBCore:
        -- local Player = QBCore.Functions.GetPlayerByCitizenId(userId)
        -- if Player then
        --     local ci = Player.PlayerData.charinfo
        --     return ci.firstname .. " " .. ci.lastname
        -- end
        -- return "Desconhecido"
    end
    ```
  </Accordion>

  <Accordion title="Permissões de admin" icon="shield">
    Controla quem pode acessar o painel administrativo (`/petsadm`). Suporta permissão única (string) ou conjunto de permissões (table).

    ```lua theme={null}
    -- Retorna true se o jogador tiver a permissão informada
    function Functions.hasPermission(userId, permission)
        if not permission then return true end

        -- Exemplo vRP (Creative Enchanted) — suporta string ou table:
        if type(permission) == "table" then
            local result = vRP.HasTable(userId, permission)
            if type(result) == "number" then return result >= 1 end
            return result == true
        end
        local result = vRP.HasPermission(userId, permission)
        if type(result) == "number" then return result >= 1 end
        return result == true

        -- Exemplo vRPex / vRP padrão:
        -- return vRP.hasPermission(userId, permission)

        -- Exemplo ESX (por grupo):
        -- local xPlayer = ESX.GetPlayerFromIdentifier(userId)
        -- return xPlayer and xPlayer.getGroup() == permission or false

        -- Exemplo QBCore:
        -- local Player = QBCore.Functions.GetPlayerByCitizenId(userId)
        -- return Player and Player.PlayerData.permission == permission or false

        -- Exemplo standalone (ace permissions):
        -- return IsPlayerAceAllowed(tostring(Functions.getUserSource(userId)), permission)
    end
    ```

    <Tip>
      O valor de `permission` verificado é o definido em `Pets.AdminPermission` no `config/config.lua`. Ajuste-o para corresponder ao sistema de permissões do seu framework (ex: `"admin"`, `"superadmin"`, `"god"`).
    </Tip>
  </Accordion>

  <Accordion title="Inventário" icon="box">
    Funções de leitura e escrita no inventário do jogador. São usadas pela aba de **Depósito/Saque** do painel e pelo handler de "usar item" para dar a pelúcia diretamente do inventário.

    ```lua theme={null}
    -- Retorna a tabela de inventário do jogador
    function Functions.getInventory(userId)
        -- Exemplo vRP (Creative Enchanted):
        return vRP.Inventory(userId)
    end

    -- Retorna (amount, name, slot) de um item específico no inventário
    function Functions.getInventoryItemAmount(userId, item)
        -- Exemplo vRP (Creative Enchanted):
        local data = vRP.InventoryItemAmount(userId, item) or {}
        return tonumber(data[1]) or 0, data[2] or item, data[3]
    end

    -- Remove um item do inventário do jogador. Retorna true em sucesso.
    function Functions.tryGetInventoryItem(userId, item, amount)
        -- Exemplo vRP (Creative Enchanted) com fallback por slot:
        if vRP.TakeItem(userId, item, amount, true) then
            return true
        end
        for _ = 1, amount do
            local itemAmount, itemName = Functions.getInventoryItemAmount(userId, item)
            if itemAmount <= 0 or itemName == "" then return false end
            if not vRP.TakeItem(userId, itemName, 1, false) then return false end
        end
        return true

        -- Exemplo vRPex / vRP padrão:
        -- return vRP.tryGetInventoryItem(userId, item, amount, true)

        -- Exemplo ESX:
        -- local xPlayer = ESX.GetPlayerFromIdentifier(userId)
        -- if xPlayer and xPlayer.getInventoryItem(item).count >= amount then
        --     xPlayer.removeInventoryItem(item, amount); return true
        -- end
        -- return false

        -- Exemplo QBCore:
        -- local Player = QBCore.Functions.GetPlayerByCitizenId(userId)
        -- return Player and Player.Functions.RemoveItem(item, amount) or false
    end

    -- Adiciona um item ao inventário do jogador
    function Functions.giveInventoryItem(userId, item, amount)
        -- Exemplo vRP (Creative Enchanted):
        return vRP.GenerateItem(userId, item, amount, true)

        -- Exemplo vRPex / vRP padrão:
        -- return vRP.giveInventoryItem(userId, item, amount, true)

        -- Exemplo ESX:
        -- local xPlayer = ESX.GetPlayerFromIdentifier(userId)
        -- if xPlayer then xPlayer.addInventoryItem(item, amount) end

        -- Exemplo QBCore:
        -- local Player = QBCore.Functions.GetPlayerByCitizenId(userId)
        -- if Player then Player.Functions.AddItem(item, amount) end
    end
    ```
  </Accordion>

  <Accordion title="Anticheat (spawn de prop)" icon="shield-halved">
    Chamada server-side **antes** de qualquer prop ser criado no client. Use para whitelistar o modelo no seu anticheat. Retornar `false` aborta o spawn para aquele jogador.

    ```lua theme={null}
    function Functions.OnPropSpawn(source, data)
        -- Exemplo com PL_PROTECT:
        -- exports["PL_PROTECT"]:setSpawnClient(source, data.prop)
        return true
    end
    ```

    **Campos disponíveis em `data`**

    | Campo       | Tipo     | Descrição                    |
    | ----------- | -------- | ---------------------------- |
    | `prop`      | `string` | Nome do modelo do prop       |
    | `bone`      | `number` | Bone de attach no ped        |
    | `posX/Y/Z`  | `number` | Posição de offset            |
    | `rotX/Y/Z`  | `number` | Rotação de offset            |
    | `dict`      | `string` | Dicionário de animação       |
    | `anim`      | `string` | Nome da animação             |
    | `flag`      | `number` | Flag da animação             |
    | `itemKey`   | `string` | Chave da pelúcia no catálogo |
    | `requestId` | `string` | ID único da requisição       |

    <Tip>
      Se o seu anticheat não precisa de integração explícita para props, basta manter `return true` sem alterações.
    </Tip>
  </Accordion>

  <Accordion title="Notificações" icon="bell">
    Disparada server-side via `Functions.notify(source, type, message, duration)`. O tipo informado (`success`, `denied`, `warning`, `info`, `error`) é traduzido para a cor esperada pelo seu sistema antes de chamar o evento de notificação.

    ```lua theme={null}
    -- Mapeia o tipo lógico para a cor esperada pelo seu evento de notificação
    local NOTIFY_BASE_TYPES = {
        success = "verde",
        denied  = "vermelho",
        warning = "amarelo",
        info    = "azul",
        error   = "vermelho",
    }

    function Functions.notify(source, notifyType, message, duration)
        if not source then return false end
        local normalizedType = string.lower(tostring(notifyType or "info"))
        local text  = tostring(message or "")
        local timer = duration or 5000

        -- Adapte o nome do evento para o seu framework:
        --   ex: "Notify", "esx:showNotification", "QBCore:Notify"
        TriggerClientEvent("Notify", source, "Pets", text,
            NOTIFY_BASE_TYPES[normalizedType] or normalizedType, timer)
        return true
    end
    ```

    <Info>
      No client-side, se o **ox\_lib** estiver carregado, o Five Pets utiliza `lib.notify` automaticamente para suas notificações internas — sem necessidade de alteração.
    </Info>
  </Accordion>

  <Accordion title="Banco de dados" icon="database">
    Wrappers que isolam o acesso ao MySQL. A implementação padrão usa o **oxmysql**, mas você pode trocar por outro driver mantendo a assinatura.

    ```lua theme={null}
    function Functions.query(command, params)
        return MySQL.query.await(command, params or {})
    end

    function Functions.update(command, params)
        return MySQL.update.await(command, params or {})
    end

    function Functions.single(command, params)
        if MySQL.single and MySQL.single.await then
            return MySQL.single.await(command, params or {})
        end
        local rows = Functions.query(command, params)
        return rows and rows[1] or nil
    end
    ```

    <Tip>
      As tabelas do recurso são criadas automaticamente na primeira inicialização, então essas funções precisam estar funcionando antes da primeira chamada de `ensure core-pets`.
    </Tip>
  </Accordion>
</AccordionGroup>

***

## Configuração

<AccordionGroup>
  <Accordion title="config/config.lua — Configurações principais" icon="sliders">
    **Comandos e permissões**

    ```lua theme={null}
    Pets.Command         = "pets"     -- Abre o painel do jogador
    Pets.AdminCommand    = "petsadm"  -- Abre o painel de admin
    Pets.AdminPermission = "Admin"    -- Permissão necessária para admin
    Pets.ThemeColor      = "#0143bb"  -- Cor principal da interface (hex)
    ```

    **Funcionalidades (liga/desliga subsistemas)**

    ```lua theme={null}
    Pets.Features = {
        Benefits = true,  -- false: exports neutros (0/1.0), GrantXP no-op, UI sem benefícios
        Fusion   = true,  -- false: aba "Aprimoramento" some e os handlers rejeitam
        Trade    = true,  -- false: aba "Trocas" some e os handlers rejeitam
    }
    ```

    Cada flag em `false` transforma o subsistema em **no-op**, sem mexer na coleção do jogador. Os pets continuam podendo ser equipados normalmente.

    **Objetos 3D (defaults usados quando o catálogo não especifica)**

    ```lua theme={null}
    Pets.Objects = {
        DefaultBone = 28422,
        DefaultFlag = 49,
    }
    ```

    **Fusão**

    ```lua theme={null}
    Pets.Fusion = {
        BaseChance        = 60,  -- Chance base quando target = maior raridade dos inputs (%)
        RarityJumpPenalty = 25,  -- Penalidade por tier acima do natural (%)
        SameRarityBonus   = 10,  -- Bônus se ambas têm a mesma raridade (%)
        MaxChance         = 90,
        MinChance         = 1,
    }
    ```

    **Raridades, benefícios e XP por nível**

    Definidos em `Pets.Rarities`, `Pets.Benefits` e `Pets.XpPerLevel`. Para cada raridade você ajusta `multiplier`, `dropWeight`, `maxLevel` e `benefitCount`. Para cada benefício, define `type` (`flat` ou `percent`), os 10 valores de `levels`, o `label` exibido na UI, o `poolWeight` no sorteio e o `xpPerAction` concedido por chamada do export `GrantXP`.
  </Accordion>

  <Accordion title="config/catalog.lua — Catálogo de pelúcias" icon="list">
    Define todas as pelúcias disponíveis. As chaves de raridade aceitas são `commom`, `uncommom`, `rare`, `epic` e `legendary` — exatamente como em `Pets.Rarities`.

    **Variante single-position** — uma única posição de equip:

    ```lua theme={null}
    ["chave_unica"] = {
        type     = "collection",
        name     = "Nome Exibido",
        category = "Categoria",
        rarity   = "rare",
        icon     = "https://cdn.fivenetwork.dev/...",
        prop = {
            model  = "nome_do_modelo",
            loop   = true,
            moving = true,
            bone   = 24818,
            pos    = { x = 0.30, y = 0.04, z = -0.30 },
            rot    = { x = -90.0, y = -90.0, z = 0.0 },
            anim   = { dict = "petcarinhoanimation@joao", name = "carinho_clip", flag = 49 }
        }
    }
    ```

    **Variante multi-position** — várias posições selecionáveis pelo jogador:

    ```lua theme={null}
    ["chave_unica"] = {
        type     = "collection",
        name     = "Nome Exibido",
        category = "Categoria",
        rarity   = "legendary",
        icon     = "https://cdn.fivenetwork.dev/...",
        prop = {
            model  = "modelo_padrao",
            loop   = true,
            moving = true,
            positions = {
                {
                    id    = "na_mao",
                    label = "Na Mão",
                    bone  = 12844,
                    pos   = { x = 0.29, y = 0.08, z = 0.0 },
                    rot   = { x = 0.0,  y = -270.0, z = 180.0 },
                    model = "modelo_da_posicao"   -- opcional, sobrescreve o model raiz
                },
                -- outras posições…
            }
        }
    }
    ```

    Em ambas as variantes os campos `bone`/`flag` caem para os defaults de `Pets.Objects` quando não preenchidos.
  </Accordion>

  <Accordion title="config/webhooks.lua — Webhooks do Discord" icon="discord">
    ```lua theme={null}
    webhookURLs = {
        give     = "",  -- Item adicionado via admin
        remove   = "",  -- Item removido via admin
        trade    = "",  -- Proposta enviada, aceita ou recusada
        featured = "",  -- Destaque alterado
        deposit  = "",  -- Depósito no painel
        withdraw = "",  -- Saque do painel
        fusion   = "",  -- Resultado de fusão (sucesso ou falha)
    }
    ```

    Deixe qualquer URL como `""` para desativar aquele webhook.
  </Accordion>
</AccordionGroup>
