Skip to content

Branching conversation

This example combines speakers, a conditional choice, a branch, an action, and a session result.

Register game handlers

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local MrDialogue = require(ReplicatedStorage.Packages.MrDialogue)

local playerState = {}

MrDialogue.RegisterCondition("hasCoins", function(context, amount)
    local state = playerState[context.player]
    return state ~= nil and state.coins >= amount
end)

MrDialogue.RegisterCondition("alreadyBoughtMap", function(context)
    local state = playerState[context.player]
    return state ~= nil and state.hasMap
end)

MrDialogue.RegisterAction("buyMap", function(context, price)
    local state = playerState[context.player]
    if not state or state.coins < price then
        return false, "not enough coins"
    end

    state.coins -= price
    state.hasMap = true
    return true
end)

Production games should connect these handlers to their authoritative profile or inventory system. The table above keeps the example focused on dialogue flow.

Define the merchant

local merchantDialogue = MrDialogue.new({
    format = 1,
    id = "merchant_map",
    entry = "check_purchase",

    speakers = {
        merchant = {
            name = "Map Merchant",
            icon = "rbxassetid://123456789",
            emotions = {
                happy = "rbxassetid://234567891",
            },
        },
    },
    defaultSpeaker = "merchant",
    freezeCharacter = true,

    nodes = {
        check_purchase = {
            type = "branch",
            condition = {
                name = "alreadyBoughtMap",
            },
            onTrue = "already_owned",
            onFalse = "offer",
        },

        already_owned = {
            type = "line",
            text = "Take good care of that map.",
            next = "leave",
        },

        offer = {
            type = "choice",
            text = "A map of the valley costs 25 coins.",
            options = {
                {
                    id = "buy",
                    text = "Buy the map.",
                    next = "purchase",
                    condition = {
                        name = "hasCoins",
                        arguments = { 25 },
                    },
                },
                {
                    id = "ask",
                    text = "What is on the map?",
                    next = "explain",
                },
                {
                    id = "leave",
                    text = "Maybe later.",
                    next = "leave",
                },
            },
        },

        explain = {
            type = "line",
            text = "Every road, ruin, and shortcut I could find.",
            next = "offer",
        },

        purchase = {
            type = "action",
            action = {
                name = "buyMap",
                arguments = { 25 },
            },
            next = "purchased",
        },

        purchased = {
            type = "line",
            text = "A fine purchase. May it keep you on the path.",
            emotion = "happy",
            next = "bought",
        },

        bought = {
            type = "end",
            result = "map_bought",
        },

        leave = {
            type = "end",
            result = "left",
        },
    },
})

The buy option is absent when the player has fewer than 25 coins. The ask path loops back only after presenting a visible line, so it is not a silent resolution cycle.

Start and inspect

script.Parent.Triggered:Connect(function(player)
    playerState[player] = playerState[player] or {
        coins = 50,
        hasMap = false,
    }

    local session, startError = merchantDialogue:Start(player, {
        npc = script.Parent:FindFirstAncestorOfClass("Model"),
    })

    if not session then
        warn(startError)
        return
    end

    task.spawn(function()
        while session:IsActive() do
            task.wait()
        end

        local outcome = session:GetOutcome()
        if outcome and outcome.status == "completed" then
            print(player.Name, outcome.result)
        end
    end)
end)

The polling task is included only to demonstrate GetOutcome(). Application code can usually react inside actions or a custom adapter instead of creating one polling task per conversation.