[Tutorial] Workbench MOD (OTClient + TFS 1.4.2)

The Crafting Table is a mod that adds an item crafting interface to the game. It works as a visual guide, making it easier for players to craft various items in a simple and intuitive way.

💻Client Side

Create a folder named crafting_table inside Client\mods. Inside it, create a folder named imgs, and inside imgs, create another folder called materiais. Still inside the crafting_table folder, create the following files: configs.lua, tutorial.lua, tutorial.otmod, and tutorial.otui.

config.lua
tutorialsIndex = {"Construction", "Tools", "Box's", "Grounds", "Building Materials", "Traps"}
-- Cores das categorias (você pode editar aqui)
categoryColors = {
    ["Construction"] = "black",        
    ["Tools"] = "black",          
    ["Box's"] = "black",          
    ["Grounds"] = "black",        
    ["Building Materials"] = "black", 
    ["Traps"] = "black"           
}

-- Cor padrão para categorias não definidas
defaultCategoryColor = "#ffffff"

tutorialsInfo = {
    -- Fances
    {
        {name = "Brown Fance", id = 10001, material = {'Wood'}, text = "[ENG]: A simple straight fence for marking areas. When used, it transforms into a different shape.\n[BRA]: Uma cerca reta simples para demarcar areas. Quando usada, ela se transforma em uma forma diferente."},
        {name = "Horizontal Farm Door", id = 10002, material = {'8 Wood'}, text = "[ENG]: A horizontal door for your farm. When used, it opens and closes.\n[BRA]: Uma porta horizontal para sua fazenda. Quando usada, ela abre e fecha."},
        {name = "Vertical Farm Door", id = 10003, material = {'8 Wood'}, text = "[ENG]: A vertical door for your farm. When used, it opens and closes.\n[BRA]: Uma porta vertical para sua fazenda. Quando usada, ela abre e fecha."},
    },
    -- Tools
    {
        {name = "Watering Can", id = 10004, material = {'8 Bucket'}, text = "[ENG]: A watering can with 90 charges.\n[BRA]: Um regador com 90 cargas."},
        {name = "Wood Pick", id = 10005, material = {'2 Refined Wood', '3 Wood'}, text = "[ENG]: A watering can with 90 charges.\n[BRA]: Um regador com 90 cargas."},
        {name = "Spray", id = 10010, material = {'1 Spray Empyt', '20 Nim'}, text = "[ENG]: Mix it all and you get the poison to kill the pests.\n[BRA]: Misturando tudo, voce consegue o veneno para matar as pestes."},
    },
    -- Box's
    {
        {name = "Cow Box", id = 10011, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with cow face design (20 slots).\n[BRA]: Uma caixa com aparencia de vaca (20 slots)."},
        {name = "Chicken Box", id = 10012, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with chicken face design (20 slots).\n[BRA]: Uma caixa com aparencia de galinha (20 slots)."},
        {name = "Fish Box", id = 10013, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with fish design (20 slots).\n[BRA]: Uma caixa com aparencia de peixe (20 slots)."},
        {name = "Seeds Box", id = 10014, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with seed pattern (20 slots).\n[BRA]: Uma caixa com aparencia de sementes (20 slots)."},
        {name = "Box", id = 10015, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A simple wooden box (20 slots).\n[BRA]: Uma caixa simples de madeira (20 slots)."},
        {name = "Pumpkin Box", id = 10016, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with pumpkin design (20 slots).\n[BRA]: Uma caixa com aparencia de abobora (20 slots)."},
        {name = "Tool Box", id = 10017, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with tool design (20 slots).\n[BRA]: Uma caixa com aparencia de ferramentas (20 slots)."},
        {name = "Changin Box", id = 10018, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A customizable box (20 slots).\n[BRA]: Uma caixa personalizavel (20 slots)."}
    },
    -- Grounds
    {
        {name = "Stone Floor", id = 10008, material = {'10 Stone Fragment'}, text = "[ENG]: A beautiful Stone floor, perfect for elegant constructions.\n[BRA]: Um lindo piso de pedras, ideal para construcoes elegantes."},
    },

    -- Materiais
    {
        {name = "Refined Wood", id = 10006, material = {'2 Wood'}, text = "[ENG]: High-quality wood, perfect for crafting furniture and tools.\n[BRA]: Madeira de alta qualidade, perfeita para criar moveis e ferramentas."},
        {name = "Board", id = 10007, material = {'4 Refined Wood'}, text = "[ENG]: A strong and versatile board, perfect for any kind of construction or crafting.\n[BRA]: Uma tabua resistente e versatil, perfeita para qualquer tipo de construcao ou criacao."},
        {name = "Nail", id = 10019, material = {'1 Iron Fragment'}, text = "[ENG]: A simple nail, essential for woodworking and construction. (2 items)\n[BRA]: Um prego simples, essencial para marcenaria e construcao. (2 itens)"}
    
    },
    -- Traps
    {
        {name = "Chiken Trap", id = 10009, material = {'20 Iron Fragment', '10 Board'}, text = "[ENG]: A trap designed to catch chickens. Set it and wait for the cluck!\n[BRA]: Uma armadilha feita para capturar galinhas. Arme e espere o cocorico!"},
       
    }

}
tutorial.lua
tutorialOpen       = false
currentLabel       = false
currentCategorie   = false
maxCategories      = 0

local form = {
    assignWindow = nil,
    oakImg       = nil,
    indexList    = nil,
    lines        = {},  
    materialImages = {}, -- Array para armazenar as imagens dos materiais
}

local config = {
  categoriesColor = "#afa9a0",
}

function setCategoriesColor()
    for x = 1, #tutorialsIndex do
        local categoryName = tutorialsIndex[x]
        local color = categoryColors[categoryName] or defaultCategoryColor
        form.indexList:getChildById("index" .. x):setColor(color)
    end
end

-- Função para limpar as imagens dos materiais
function clearMaterialImages()
    for i, img in ipairs(form.materialImages) do
        if img then
            img:destroy()
        end
    end
    form.materialImages = {}
end

-- Função para criar as imagens dos materiais
function createMaterialImages(materials)
    clearMaterialImages()
    
    if not materials then return end
    
    local imgSize = 32 -- Tamanho das imagens dos materiais
    local verticalSpacing = 3  -- Espaçamento vertical entre materiais
    local textMargin = 5  -- Espaçamento entre a imagem e o texto
    local headerHeight = 20 -- Altura do cabeçalho
    
    -- Pega o painel da imagem principal
    local mainPanel = tutorialWindow:getChildById('tutorialImg')
    
    -- Calcula a altura total necessária para todos os materiais (incluindo o cabeçalho)
    local totalHeight = headerHeight + #materials * (imgSize + verticalSpacing) - verticalSpacing
    
    -- Cria o cabeçalho "Materials Required"
    local headerPanel = g_ui.createWidget('Panel', mainPanel)
    headerPanel:setId("materialsHeader")
    headerPanel:setSize({width = 200, height = headerHeight})
    headerPanel:addAnchor(AnchorLeft, 'parent', AnchorLeft)
    headerPanel:addAnchor(AnchorTop, 'parent', AnchorTop)
    headerPanel:setMarginTop(10)
    headerPanel:setMarginLeft(10)
    
    local headerLabel = g_ui.createWidget('MaterialText', headerPanel)
    headerLabel:setId("headerText")
    headerLabel:setText("Materials Required")
    headerLabel:setColor("#ffffff") -- Texto branco para destacar
    headerLabel:addAnchor(AnchorLeft, 'parent', AnchorLeft)
    headerLabel:addAnchor(AnchorVerticalCenter, 'parent', AnchorVerticalCenter)
    
    table.insert(form.materialImages, headerPanel)
    
    for i, material in ipairs(materials) do
        -- Extrai o nome do material (remove a quantidade se existir)
        local materialName = material:match("^%d+%s*(.+)") or material
        local quantity = material:match("^(%d+)%s*") or "1"
        
        -- Cria um painel container para cada material
        local containerPanel = g_ui.createWidget('Panel', mainPanel)
        containerPanel:setId("materialContainer" .. i)
        containerPanel:setSize({width = 200, height = imgSize}) -- Largura maior para acomodar o texto
        containerPanel:addAnchor(AnchorLeft, 'parent', AnchorLeft)
        containerPanel:addAnchor(AnchorTop, 'parent', AnchorTop)
        -- Calcula a posição de baixo para cima (agora considerando o cabeçalho)
        containerPanel:setMarginTop(totalHeight - (i * (imgSize + verticalSpacing)) + 10)
        containerPanel:setMarginLeft(10)
        
        -- Cria a imagem do material
        local materialImg = g_ui.createWidget('MaterialImage', containerPanel)
        materialImg:setId("materialImg" .. i)
        materialImg:setSize({width = imgSize, height = imgSize})
        materialImg:addAnchor(AnchorLeft, 'parent', AnchorLeft)
        materialImg:addAnchor(AnchorVerticalCenter, 'parent', AnchorVerticalCenter)
        materialImg:setImageSource("imgs/materiais/" .. materialName)
        
        -- Cria o label com a quantidade e nome
        local textLabel = g_ui.createWidget('MaterialText', containerPanel)
        textLabel:setId("materialText" .. i)
        textLabel:setText(quantity .. "x " .. materialName)
        textLabel:setMarginLeft(imgSize + textMargin) -- Posiciona o texto após a imagem
        textLabel:addAnchor(AnchorLeft, 'parent', AnchorLeft)
        textLabel:addAnchor(AnchorVerticalCenter, 'parent', AnchorVerticalCenter)
        
        table.insert(form.materialImages, containerPanel)
    end
end

tutorialsIndex = nil
tutorialsInfo = nil

dofile('configs.lua')
tutorialsIndex = tutorialsIndex
tutorialsInfo  = tutorialsInfo
maxCategories  = #tutorialsIndex

function init()    
    tutorialWindow = g_ui.loadUI('tutorial', rootWidget)
    tutorialWindow:hide()
    form.assignWindow = tutorialWindow:getChildById('tutorialImg')
    form.oakImg       = tutorialWindow:getChildById('oak')
    form.indexList    = tutorialWindow:getChildById('indexList')

    -- Adiciona eventos
    connect(g_game, {
        onGameStart = onGameStart,
        onGameEnd = hideAll,
        onWalk = onWalk,
        onAutoWalk = onAutoWalk
    })

    -- Adiciona handler para extended opcode
    ProtocolGame.registerExtendedOpcode(50, function(protocol, opcode, buffer)
        if buffer == "open_craft" then
            if not tutorialOpen then
                onOpenTutorial()
            end
        end
    end)

    -- Popular a lista de categorias e tutoriais
    for index, widget in pairs(form.lines) do
        widget:destroy()
    end    
    form.lines = {}
    for sectionIndex, section in pairs(tutorialsIndex) do
        local label = g_ui.createWidget('TutorialLabel', form.indexList)
        label:setId("index" .. sectionIndex)
        label.index = sectionIndex
        label:setText(section)
        -- Usa a cor personalizada da categoria
        local color = categoryColors[section] or defaultCategoryColor
        label:setColor(color)
        table.insert(form.lines, label)
        for idx, currentTutorial in pairs(tutorialsInfo[sectionIndex]) do
            local labelTree = g_ui.createWidget('TutorialLabel', form.indexList)
            labelTree.img   = currentTutorial.img
            labelTree:setId("index" .. sectionIndex .. "labelTree" .. idx)
            labelTree.description = currentTutorial.text
            labelTree:setText("    " .. currentTutorial.name)
            labelTree:setVisible(false)
            labelTree.index      = sectionIndex
            labelTree.label      = idx
            labelTree.isTutorial = true
            table.insert(form.lines, labelTree)
        end
    end

    connect(form.indexList, {
        onChildFocusChange = changeFocusIndexList
    })

    g_keyboard.bindKeyPress('Up',   nextTutorial,  tutorialWindow)
    g_keyboard.bindKeyPress('Down', priorTutorial, tutorialWindow)    

    local quantityBar = tutorialWindow:getChildById('scrollablePainel'):getChildById('quantityPanel'):getChildById('quantityBar')
    connect(quantityBar, { onValueChange = onQuantityBarValueChange })
end

function onGameStart()
    print("[DEBUG] Game started")
end

function setToDefault(state)
    if state then
        tutorialWindow:getChildById('scrollablePainel'):setVisible(false)
        tutorialWindow:getChildById('textScroll'):setVisible(false)
        form.assignWindow:setHeight(375)

        form.assignWindow:setImageSource("imgs/default")
        form.oakImg:setVisible(true)
        
        -- Limpa as imagens dos materiais
        clearMaterialImages()
    else
        form.assignWindow:setHeight(206)
        tutorialWindow:getChildById('scrollablePainel'):setVisible(true)
        tutorialWindow:getChildById('textScroll'):setVisible(true)
        form.oakImg:setVisible(false)
    end
end

function changeFocusIndexList(self, focusedChild)
    if focusedChild == nil then 
        tutorialWindow:getChildById('scrollablePainel'):getChildById('quantityPanel'):setVisible(false)
        return 
    end

    for x = 1, #tutorialsIndex do
        if x == focusedChild.index then
            for y = 1, #tutorialsInfo[x] do
                if focusedChild.isOpen then
                    form.indexList:getChildById("index" .. x .. "labelTree" .. y):setVisible(false)
                else
                    form.indexList:getChildById("index" .. x .. "labelTree" .. y):setVisible(true)
                end
            end
            break
        end
    end

    local quantityPanel = tutorialWindow:getChildById('scrollablePainel'):getChildById('quantityPanel')
    if focusedChild.isTutorial then
        quantityPanel:setVisible(true)
    else
        quantityPanel:setVisible(false)
    end

    if focusedChild.isTutorial then
        setToDefault(false)
        local tutorialText = tutorialWindow:getChildById('scrollablePainel'):getChildById('tutorialText')
        tutorialText:setText(focusedChild.description)
        
        -- Gera automaticamente o caminho da imagem a partir do nome
        local imageName = focusedChild:getText():gsub("^%s+", ""):gsub("%s+$", "") -- Remove espaços no início e fim
        local imagePath = "imgs/" .. imageName
        form.assignWindow:setImageSource(imagePath)
        
        -- Cria as imagens dos materiais
        local currentTutorial = tutorialsInfo[focusedChild.index][focusedChild.label]
        if currentTutorial and currentTutorial.material then
            createMaterialImages(currentTutorial.material)
        end

        currentLabel = focusedChild.label
        currentCategorie = focusedChild.index
    else
        if focusedChild.isOpen then focusedChild.isOpen = false else focusedChild.isOpen = true end
        setToDefault(true)
        currentCategorie = false

        if focusedChild.isOpen then
            form.indexList:focusChild(form.indexList:getChildById("index"..focusedChild.index.."labelTree1"))
        else
            form.indexList:focusChild(nil)
        end
    end

    setCategoriesColor()
end

function nextTutorial()
    if currentLabel and currentCategorie then
        local child = form.indexList:getChildById("index"..currentCategorie.."labelTree"..currentLabel-1)

        if child then
            form.indexList:focusChild(child)
        else
            if currentCategorie-1 >= 1 then
                form.indexList:getChildById("index"..currentCategorie-1).isOpen = true
                form.indexList:focusChild(form.indexList:getChildById("index"..(currentCategorie-1).."labelTree"..(#tutorialsInfo[currentCategorie-1])))
            end
        end
    end
end

function priorTutorial()
    if currentLabel and currentCategorie then
        local child = form.indexList:getChildById("index"..currentCategorie.."labelTree"..currentLabel+1)

        if child then
            form.indexList:focusChild(child)
        else
            if currentCategorie+1 <= maxCategories then
                form.indexList:getChildById("index"..currentCategorie+1).isOpen = true
                form.indexList:focusChild(form.indexList:getChildById("index"..(currentCategorie+1).."labelTree1"))
            end
        end
    else
        form.indexList:getChildById("index1").isOpen = true
        form.indexList:focusChild(form.indexList:getChildById("index1labelTree1"))
    end    
end

function destroyAll()
    tutorialWindow:destroy()
end

function hideAll()
    tutorialWindow:hide()
end

function terminate()
    disconnect(g_game, { 
        onGameEnd = hideAll,
        onGameStart = onGameStart,
        onWalk = onWalk,
        onAutoWalk = onAutoWalk
    })

    clearMaterialImages()
    destroyAll()
end

function onOpenTutorial()
    form.indexList:focusChild(nil)
    tutorialOpen = true
    tutorialWindow:show()
    tutorialWindow:raise()
    tutorialWindow:focus()

    setToDefault(true)
    setCategoriesColor()
    currentLabel = false
    currentCategorie = false
end

function onClickTutorial()
    if not tutorialOpen then
        onOpenTutorial()    
        local node = g_settings.getNode('firsttimeseta') or {}
        if node then
            if not node['tutorialWindow'] then
                node['tutorialWindow'] = true
                g_settings.setNode('firsttimeseta', node)
            end      
        end 
    else              
        tutorialOpen = false
        tutorialWindow:hide()
    end
end

function onQuantityBarValueChange()
    local quantityBar = tutorialWindow:getChildById('scrollablePainel'):getChildById('quantityPanel'):getChildById('quantityBar')
    local quantityValue = tutorialWindow:getChildById('scrollablePainel'):getChildById('quantityPanel'):getChildById('quantityValue')
    quantityValue:setText(tostring(quantityBar:getValue()))
end

function onCreateClicked()
    local painel = tutorialWindow:getChildById('scrollablePainel'):getChildById('quantityPanel')
    local quantityBar = painel:getChildById('quantityBar')
    local quantidade = quantityBar:getValue()

    local itemId = 0
    if currentLabel and currentCategorie then
        itemId = tutorialsInfo[currentCategorie][currentLabel].id
    end

    if itemId > 0 then
        local msg = "!craft " .. itemId .. " " .. tostring(quantidade)
        print("[DEBUG CLIENTE] Enviando talkaction: " .. msg)
        g_game.talk(msg)
    end
end

function onWalk(direction)
    if tutorialOpen then
        tutorialOpen = false
        tutorialWindow:hide()
        return false
    end
    return true
end

function onAutoWalk(path)
    if tutorialOpen then
        tutorialOpen = false
        tutorialWindow:hide()
        return false
    end
    return true
end
tutorial.otmod
Module
  name: crafting_table
  description: Crafting Table
  author: 
  sandboxed: true
  scripts: [tutorial]
  autoload-priority: 1001
  autoload: true
  @onLoad: init()
  @onUnload: terminate()
tutorial.otui
TutorialLabel < Label
  font: verdana-11px-monochrome
  text-offset: 2 0
  focusable: true
  background-color: alpha
  
  $focus:
    background-color: #444444

  $on focus:
    color: #00ff00
  $!on focus:
    color: #ffffff

MainWindow
  size: 725 400
  padding: 0
  @onEscape: modules.crafting_table.onClickTutorial()
  
  Panel
    id: tutorialImg
    anchors.top: parent.top
    anchors.right: parent.right
    margin-right: 10
    margin-top: 25
    size: 520 220
    border-width: 2
    border-color: #201f1f
    background-color: #00000066

  Panel
    id: oak
    anchors.top: parent.top
    anchors.right: parent.right
    margin-right: 163
    margin-top: 153
    __image-source: imgs/default1

  TextList
    id: indexList
    anchors.top: parent.top
    anchors.left: parent.left
    anchors.bottom: parent.bottom
    margin-left: 10
    margin-top: 25
    margin-bottom: 10
    focusable: false
    size: 180 180
    vertical-scrollbar: indexScroll
    focusable: true
    phantom: false

  ScrollablePanel
    id: scrollablePainel
    anchors.bottom: parent.bottom
    anchors.right: parent.right
    margin-right: 10
    margin-bottom: 10
    size: 520 155
    background-color: #201f1f
    vertical-scrollbar: textScroll
    padding: 5

    Label
      id: tutorialText
      text-wrap: true
      text-auto-resize:true
      anchors.right: parent.right
      anchors.top: parent.top
      anchors.left: parent.left
      margin-right: 13
      focusable: true
      phantom: false

    Panel
      id: quantityPanel
      visible: false
      anchors.left: parent.left
      anchors.right: parent.right
      anchors.bottom: parent.bottom
      margin-bottom: 0
      height: 32
      background-color: alpha

      HorizontalScrollBar
        id: quantityBar
        anchors.left: parent.left
        anchors.verticalCenter: parent.verticalCenter
        width: 300
        minimum: 1
        maximum: 100
        step: 1
        value: 1

      Label
        id: quantityValue
        anchors.left: quantityBar.right
        anchors.verticalCenter: parent.verticalCenter
        margin-left: 8
        text: "1"
        width: 30
        text-align: center

      UIButton
        id: createButton
        anchors.right: parent.right
        anchors.verticalCenter: parent.verticalCenter
        margin-right: 12
        text: "Criar"
        width: 80
        height: 24
        @onClick: onCreateClicked()
        background-color: #3c3c3c
        border-width: 1
        border-color: #8f8f8f
        color: #ffffff
        font: verdana-11px-monochrome

        $hover:
          background-color: #5a5a5a
          border-color: #bfbfbf
          color: #ffffa0

  VerticalScrollBar
    id: textScroll
    anchors.bottom: parent.bottom
    anchors.right: parent.right
    margin-bottom: 10
    margin-right: 10
    step: 16
    height: 156

  VerticalScrollBar
    id: indexScroll
    anchors.top: parent.top
    anchors.left: parent.left
    anchors.bottom: parent.bottom
    margin-top: 25
    margin-bottom: 10
    margin-left: 176
    step: 16
    height: 170

  UIButton
    id: cancelButton
    image-source: imgs/exit
    size: 16 16
    anchors.right: parent.right
    anchors.top: parent.top
    margin-top: 3
    margin-right: 5
    @onClick: onClickTutorial()

    $hover:
      image-source: imgs/exit_onhover     

MaterialImage < Panel
  border-width: 1
  border-color: #888888
  background-color: #222222
  margin: 2

  $focus:
    background-color: #444444

  $on focus:
    color: #00ff00
  $!on focus:
    color: #ffffff

  $hover:
    background-color: #5a5a5a
    border-color: #bfbfbf
    color: #ffffa0     

MaterialText < Label
  font: verdana-11px-monochrome
  text-auto-resize: true
  color: #ffffff

Inside the imgs folder, you will place, for example, the images that will be displayed on the right side, like this one.

Inside the imgs/materiais folder, you will place the icons needed, with a size of 32x32 pixels.

💻Server Side

Create a archive named workbench.lua (or any name you prefer) inside data/scripts.

workbench.lua
local workbenchItems = {
    26453 -- IDs dos itens que abrem o craft
}

local workbenchAction = Action()

function workbenchAction.onUse(player, item, fromPosition, target, toPosition, isHotkey)
    if table.contains(workbenchItems, item:getId()) then
        player:sendExtendedOpcode(50, "open_craft")
        return true
    end

    return false
end

-- Registra a ação para os itens
workbenchAction:id(26453)
workbenchAction:register()

I used item 26453 so that when I right-click, it opens the mod using OPCode 50. player:sendExtendedOpcode(50, "open_craft")

In Server\data\scripts\talkactions\ create crafting_table.lua

crafting_table.lua
local craftFence = TalkAction("!craft")

-- Tabela de materiais disponíveis
local materials = {
    wood = {id = 5901, name = "Wood"},
    bucket = {id = 26385, name = "Bucket"},
    iron_fragment = {id = 26491, name = "Iron Fragment"},
    gold = {id = 26401, name = "Gold"},
    stone_fragments = {id = 26489, name = "Stone"},
    leather = {id = 26403, name = "Leather"},
    refined_wood = {id = 26498, name = "Refined Wood"},
    board = {id = 26499, name = "Board"},
    spray_empyt = {id = 26546, name = "Spray Empyt"},
    nim = {id = 26544, name = "Nim"},
    nail = {id = 8309, name = "Nail"},
}

-- Configuração de cargas para itens que precisam
local itemCharges = {
    [26470] = 90,   -- Regador Vazio
    [26484] = 50,   -- Wood Pick
    [26539] = 100,  -- spray
}

local recipes = {
    -- Cercas e Portões
    [10001] = {materials = {wood = 6}, itemid = 26454, name = "Cerca Marrom", level = 1, quant = 1},
    [10002] = {materials = {wood = 8}, itemid = 26460, name = "Porteira Horizontal", level = 1, quant = 1},
    [10003] = {materials = {wood = 8}, itemid = 26466, name = "Porteira Vertical", level = 1, quant = 1},
    
    -- Ferramentas
    [10004] = {materials = {bucket = 8}, itemid = 26470, name = "Regador Vazio", level = 1, quant = 1},
    [10005] = {materials = {refined_wood = 2, wood = 3}, itemid = 26484, name = "Wood Pick", level = 1, quant = 1},
    [10010] = {materials = {spray_empyt = 1, nim = 20}, itemid = 26539, name = "spray ant pest", level = 1, quant = 1},

    -- Materiais
    [10006] = {materials = {wood = 2}, itemid = 26498, name = "Madeira Refinada", level = 1, quant = 1},
    [10007] = {materials = {refined_wood = 4}, itemid = 26499, name = "Board", level = 1, quant = 1},
    [10019] = {materials = {iron_fragment = 1}, itemid = 8309, name = "Nail", level = 1, quant = 2},

    -- Grounds
    [10008] = {materials = {stone_fragments = 10}, itemid = 26500, name = "Stone Floor", level = 1, quant = 1},

    -- Armadilhas
    [10009] = {materials = {iron_fragment = 20, board = 10}, itemid = 26534, name = "Chicken Trap", level = 30, quant = 1},

    -- Caixas
    [10011] = {materials = {nail = 5, refined_wood = 4, board = 5}, itemid = 26550, name = "Cow Box", level = 1, quant = 1},
    [10012] = {materials = {nail = 5, refined_wood = 4, board = 5}, itemid = 26551, name = "Chicken Box", level = 1, quant = 1},
    [10013] = {materials = {nail = 5, refined_wood = 4, board = 5}, itemid = 26552, name = "Fish Box", level = 1, quant = 1},
    [10014] = {materials = {nail = 5, refined_wood = 4, board = 5}, itemid = 26553, name = "Seeds Box", level = 1, quant = 1},
    [10015] = {materials = {nail = 5, refined_wood = 4, board = 5}, itemid = 26554, name = "Box", level = 1, quant = 1},
    [10016] = {materials = {nail = 5, refined_wood = 4, board = 5}, itemid = 26555, name = "Pumpkin Box", level = 1, quant = 1},
    [10017] = {materials = {nail = 5, refined_wood = 4, board = 5}, itemid = 26556, name = "Tool Box", level = 1, quant = 1},
    [10018] = {materials = {nail = 5, refined_wood = 4, board = 5}, itemid = 26557, name = "Changin Box", level = 1, quant = 1},
}

local function isNearWorkbench(player)
    local pos = player:getPosition()
    local workbenchIds = {26451, 26452}
    
    -- Verifica em um raio de 1 SQM ao redor do jogador
    for x = -1, 1 do
        for y = -1, 1 do
            local checkPos = Position(pos.x + x, pos.y + y, pos.z)
            local tile = Tile(checkPos)
            if tile then
                for _, item in ipairs(tile:getItems()) do
                    if table.contains(workbenchIds, item:getId()) then
                        return true
                    end
                end
            end
        end
    end
    return false
end

function craftFence.onSay(player, words, param)
    if not isNearWorkbench(player) then
        player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You need to be near a workbench to craft items.")
        return false
    end

    local args = param:split(" ")
    local itemId = tonumber(args[1])
    local amount = tonumber(args[2])

    local recipe = recipes[itemId]
    if not recipe then
        player:showTextDialog(6578, "Invalid item ID.")
        return false
    end

    -- Verifica o nível do jogador
    if player:getLevel() < recipe.level then
        player:showTextDialog(6578, "You need level " .. recipe.level .. " to craft this item.")
        return false
    end

    -- Verifica se o jogador tem todos os materiais necessários
    local missingMaterials = {}
    for material, requiredAmount in pairs(recipe.materials) do
        local totalRequired = requiredAmount * amount
        local materialId = materials[material].id
        
        if player:getItemCount(materialId) < totalRequired then
            local missing = totalRequired - player:getItemCount(materialId)
            table.insert(missingMaterials, {
                name = materials[material].name,
                amount = missing
            })
        end
    end

    if #missingMaterials > 0 then
        local message = "You are missing the following materials:\n"
        for _, material in ipairs(missingMaterials) do
            message = message .. "- " .. material.amount .. "x " .. material.name .. "\n"
        end
        player:showTextDialog(recipe.itemid, message)
        return false
    end

    -- Remove os materiais
    for material, requiredAmount in pairs(recipe.materials) do
        local totalRequired = requiredAmount * amount
        local materialId = materials[material].id
        player:removeItem(materialId, totalRequired)
    end

    -- Adiciona o item criado com as cargas corretas
    for i = 1, amount do
        local createdItem = player:addItem(recipe.itemid, recipe.quant or 1)
        if createdItem and itemCharges[recipe.itemid] then
            createdItem:setAttribute(ITEM_ATTRIBUTE_CHARGES, itemCharges[recipe.itemid])
        end
    end

    local totalCreated = amount * (recipe.quant or 1)
    local texto = "You crafted " .. totalCreated .. "x " .. recipe.name .. "!"
    local itemId = recipe.itemid
    player:showTextDialog(itemId, texto)
    return false
end

craftFence:separator(" ")
craftFence:register() 

👌 The system is now installed. Now I’m going to teach you how it works! To add a new category, go to the config.lua file inside the crafting_table folder in the client.

tutorialsIndex = {"Construction", "Tools", "Box's", "Grounds", "Building Materials", "Traps"}
-- Cores das categorias (você pode editar aqui)
categoryColors = {
    ["Construction"] = "black",        
    ["Tools"] = "black",          
    ["Box's"] = "black",          
    ["Grounds"] = "black",        
    ["Building Materials"] = "black", 
    ["Traps"] = "black"           
}

in tutorialsIndex add new category exemple Bloc tutorialsIndex = {"Bloc", "Construction", "Tools", "Box's", "Grounds", "Building Materials", "Traps"}

Right below, you will see categoryColors = { Here, you can set the color you want to change it on the panel. (In the example, I set the Bloc in red.)

categoryColors = {
    ["Bloc"] = "red", -- Here New Add
    ["Construction"] = "black",        
    ["Tools"] = "black",          
    ["Box's"] = "black",          
    ["Grounds"] = "black",        
    ["Building Materials"] = "black", 
    ["Traps"] = "black"           
}

Right below, in tutorialsInfo = {, you'll find several lines like this.

 {name = "Brown Fance", id = 10001, material = {'Wood'}, text = "[ENG]: A simple straight fence for marking areas. When used, it transforms into a different shape."},

🧠 Meaning of Each Part:

  • name = "Brown Fence" → The name of the item. It will be displayed in the UI or tooltip.

  • id = 10001 → A unique ID that identifies this item in the system.

  • material = {'Wood'} → The materials required to craft this item. In this case, only 'Wood' is needed.

  • text = "Text Here" → The item description in two languages: The ID must always be unique because it is sent via talkaction to our script in order to create the item.

🚨Each key represents the order in which it appears in the categories. 'Construction' refers to the items that will appear inside it, except for the ones listed in the first key. 'Whenever you create a new item, the ID must be unique because we will use it in the talkaction.'

tutorialsIndex = {"Construction", "Tools", "Box's", "Grounds", "Building Materials", "Traps"}

-- Cores das categorias (você pode editar aqui)
categoryColors = {
    ["Construction"] = "black",        
    ["Tools"] = "black",          
    ["Box's"] = "black",          
    ["Grounds"] = "black",        
    ["Building Materials"] = "black", 
    ["Traps"] = "black"           
}

-- Cor padrão para categorias não definidas
defaultCategoryColor = "#ffffff"

tutorialsInfo = {
    -- Construction
    {
        {name = "Brown Fance", id = 10001, material = {'Wood'}, text = "[ENG]: A simple straight fence for marking areas. When used, it transforms into a different shape.\n[BRA]: Uma cerca reta simples para demarcar areas. Quando usada, ela se transforma em uma forma diferente."},
        {name = "Horizontal Farm Door", id = 10002, material = {'8 Wood'}, text = "[ENG]: A horizontal door for your farm. When used, it opens and closes.\n[BRA]: Uma porta horizontal para sua fazenda. Quando usada, ela abre e fecha."},
        {name = "Vertical Farm Door", id = 10003, material = {'8 Wood'}, text = "[ENG]: A vertical door for your farm. When used, it opens and closes.\n[BRA]: Uma porta vertical para sua fazenda. Quando usada, ela abre e fecha."},
    },
    -- Tools
    {
        {name = "Watering Can", id = 10004, material = {'8 Bucket'}, text = "[ENG]: A watering can with 90 charges.\n[BRA]: Um regador com 90 cargas."},
        {name = "Wood Pick", id = 10005, material = {'2 Refined Wood', '3 Wood'}, text = "[ENG]: A watering can with 90 charges.\n[BRA]: Um regador com 90 cargas."},
        {name = "Spray", id = 10010, material = {'1 Spray Empyt', '20 Nim'}, text = "[ENG]: Mix it all and you get the poison to kill the pests.\n[BRA]: Misturando tudo, voce consegue o veneno para matar as pestes."},
    },
    -- Box's
    {
        {name = "Cow Box", id = 10011, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with cow face design (20 slots).\n[BRA]: Uma caixa com aparencia de vaca (20 slots)."},
        {name = "Chicken Box", id = 10012, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with chicken face design (20 slots).\n[BRA]: Uma caixa com aparencia de galinha (20 slots)."},
        {name = "Fish Box", id = 10013, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with fish design (20 slots).\n[BRA]: Uma caixa com aparencia de peixe (20 slots)."},
        {name = "Seeds Box", id = 10014, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with seed pattern (20 slots).\n[BRA]: Uma caixa com aparencia de sementes (20 slots)."},
        {name = "Box", id = 10015, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A simple wooden box (20 slots).\n[BRA]: Uma caixa simples de madeira (20 slots)."},
        {name = "Pumpkin Box", id = 10016, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with pumpkin design (20 slots).\n[BRA]: Uma caixa com aparencia de abobora (20 slots)."},
        {name = "Tool Box", id = 10017, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A box with tool design (20 slots).\n[BRA]: Uma caixa com aparencia de ferramentas (20 slots)."},
        {name = "Changin Box", id = 10018, material = {'5 Nails', '4 Refined Wood', '5 board'}, text = "[ENG]: A customizable box (20 slots).\n[BRA]: Uma caixa personalizavel (20 slots)."}
    },
    -- Grounds
    {
        {name = "Stone Floor", id = 10008, material = {'10 Stone Fragment'}, text = "[ENG]: A beautiful Stone floor, perfect for elegant constructions.\n[BRA]: Um lindo piso de pedras, ideal para construcoes elegantes."},
    },

    -- Building Materials
    {
        {name = "Refined Wood", id = 10006, material = {'2 Wood'}, text = "[ENG]: High-quality wood, perfect for crafting furniture and tools.\n[BRA]: Madeira de alta qualidade, perfeita para criar moveis e ferramentas."},
        {name = "Board", id = 10007, material = {'4 Refined Wood'}, text = "[ENG]: A strong and versatile board, perfect for any kind of construction or crafting.\n[BRA]: Uma tabua resistente e versatil, perfeita para qualquer tipo de construcao ou criacao."},
        {name = "Nail", id = 10019, material = {'1 Iron Fragment'}, text = "[ENG]: A simple nail, essential for woodworking and construction. (2 items)\n[BRA]: Um prego simples, essencial para marcenaria e construcao. (2 itens)"}
    
    },
    -- Traps
    {
        {name = "Chiken Trap", id = 10009, material = {'20 Iron Fragment', '10 Board'}, text = "[ENG]: A trap designed to catch chickens. Set it and wait for the cluck!\n[BRA]: Uma armadilha feita para capturar galinhas. Arme e espere o cocorico!"},
       
    }

}

When the client sends the talkaction to the server, it sends something like: !craft 10001, 2 where 10001 is the unique ID and 2 is the quantity.

You might be wondering: if a player just pastes this command, will they be able to craft the item anywhere? I thought about that — the server performs a check first to verify if the player is next to the item that was right-clicked to activate the crafting system! Now let's move on to the talkaction part on the server, in the file crafting_table.lua

Here, in the materials key, you can see the materials used for crafting — to be more specific, it’s composed of the item name and its ID.

local materials = {
    wood = {id = 5901, name = "Wood"},
    bucket = {id = 26385, name = "Bucket"},
    iron_fragment = {id = 26491, name = "Iron Fragment"},
    gold = {id = 26401, name = "Gold"},
    stone_fragments = {id = 26489, name = "Stone"},
    leather = {id = 26403, name = "Leather"},
    refined_wood = {id = 26498, name = "Refined Wood"},
    board = {id = 26499, name = "Board"},
    spray_empyt = {id = 26546, name = "Spray Empyt"},
    nim = {id = 26544, name = "Nim"},
    nail = {id = 8309, name = "Nail"},
}

Because I used some tools that have a charge system, like the watering can and pickaxe, I added this section.

-- Configuração de cargas para itens que precisam
local itemCharges = {
    [26470] = 90,   -- Regador Vazio
    [26484] = 50,   -- Wood Pick
    [26539] = 100,  -- spray
}

Once the item is crafted, it receives a charge.

You can simply ignore this part or leave a comment if you prefer.

Here, in local recipes = {, you can see the recipe to craft an item.

[10001] = {materials = {wood = 6}, itemid = 26454, name = "Cerca Marrom", level = 1, quant = 1},
Field
Description
Example

10001

The unique ID that was set in the client script to be activated here in the talkaction

10001

materials

Materials required to craft the item, e.g., 6 units of wood

{ wood = 6 }

You can add more materials like this.

materials = {nail = 5, refined_wood = 4, board = 5}

itemid

The item ID given to the player if they have the required materials

26454

name

A name for you to identify what the item ID represents

"Brown Fence"

level

The player level required to craft the item

1

quant

The quantity of the item

(with the given itemid)

that will be given to the player

2

Here you define which item must be around the player so that the mod cannot be opened anywhere.

local function isNearWorkbench(player)
    local pos = player:getPosition()
    local workbenchIds = {26451, 26452} -- Item Here <--------
    
    -- Verifica em um raio de 1 SQM ao redor do jogador
    for x = -1, 1 do
        for y = -1, 1 do
            local checkPos = Position(pos.x + x, pos.y + y, pos.z)
            local tile = Tile(checkPos)
            if tile then
                for _, item in ipairs(tile:getItems()) do
                    if table.contains(workbenchIds, item:getId()) then
                        return true
                    end
                end
            end
        end
    end
    return false
end

I used an item that cannot be moved, fixed in the game.


I hope this helps anyone who needs it! 😄 If you'd like to support me, here are my details:

💖 PayPal: darcio1989@gmail.com 💸 PIX: (14) 99665-9919

Any help is greatly appreciated! Thank you so much for your support! 🙏✨

Atualizado