NPC Dialogue

Give mobs interactive, branching dialogue with Lua — dialogue pages, choice buttons, and the response table format.

Mobs can present players with an interactive dialogue window: a portrait, a page of text, and up to eight choice buttons. Two Lua hooks drive it — one that opens the conversation, and one that handles each button the player clicks. The branching structure of the conversation lives entirely in your Lua.


Declaring the Hooks

Both hooks are declared inside the mob’s entry in mob-config.md:

mobs:
  - name: village_elder
    # ...
    scripts:
      server-on-dialogue-open: /mobs/elder/dialogue_open.lua
      server-on-dialogue-choice: /mobs/elder/dialogue_choice.lua

Each script file returns its function (see mob-config.md#scripts):

-- /mobs/elder/dialogue_open.lua
return function(player, npc)
  -- ...
end
HookWhen CalledSignature
server-on-dialogue-openPlayer interacts with the mobfunction(player, npc)
server-on-dialogue-choicePlayer clicks a dialogue buttonfunction(player, npc, choice_id)

player and npc are entity objects (see lua-server-entity.md); choice_id is the id string of the button the player clicked.


The Response Table

Both hooks must return a table telling the engine what to show next.

Show a dialogue page:

return {
    type = "page",
    portrait = "Creation:elder_portrait",   -- icon texture to show
    text = "Welcome, traveller.",
    buttons = {
        { id = "quest", label = "Any work for me?" },
        { id = "bye",   label = "Goodbye." },
    },
}

Close the dialogue window:

return { type = "close" }

Rules:

  • Up to 8 buttons are shown; extras are dropped.
  • A button needs a non-empty id (what your choice hook receives) and a label (what the player sees). Buttons without an id are dropped.
  • portrait, text, and buttons are all optional — missing fields show as empty. Text is capped at 2048 bytes.
  • Any malformed return (or type other than "page") closes the dialogue rather than erroring.

Full Example — Branching Conversation

-- /mobs/elder/dialogue_open.lua
return function(player, npc)
    return {
        type = "page",
        portrait = "Creation:elder_portrait",
        text = "Welcome, traveller. The mines have gone quiet lately...",
        buttons = {
            { id = "ask_mines", label = "What happened in the mines?" },
            { id = "heal",      label = "Can you patch me up?" },
            { id = "bye",       label = "Farewell." },
        },
    }
end
-- /mobs/elder/dialogue_choice.lua
return function(player, npc, choice_id)
    if choice_id == "ask_mines" then
        return {
            type = "page",
            portrait = "Creation:elder_portrait",
            text = "Something old woke beneath level thirty. Bring me proof and I'll reward you.",
            buttons = {
                { id = "bye", label = "I'll look into it." },
            },
        }
    elseif choice_id == "heal" then
        player:heal(50)
        return {
            type = "page",
            portrait = "Creation:elder_portrait",
            text = "There. Do be more careful.",
            buttons = {
                { id = "bye", label = "Thank you." },
            },
        }
    end

    return { type = "close" }
end

Use the npc’s lua_data (or the player’s) to remember conversation state between pages — for example a quest flag set in one branch and checked when the dialogue is next opened.


See Also