Authored Dialogues
Moongate supports deterministic NPC dialogue trees authored in Lua.
This feature is fully usable without OpenAI.
Use this when you want:
- quest or vendor conversations with fixed outcomes
- reusable topic routing from nearby player speech
- persistent per-NPC, per-player memory without calling OpenAI
- deterministic dialogue with no OpenAI dependency at all
Files And Layout
Recommended layout:
moongate_data/scripts/
├── common/
│ ├── dialogue.lua
│ └── npc_dialogue.lua
└── dialogs/
└── innkeeper.lua
NPC brains then bind and use those conversations from scripts/ai/npcs/**.
Conversation DSL
Use common.dialogue to register a conversation table.
local dialogue = require("common.dialogue")
return dialogue.conversation("innkeeper", {
start = "start",
topics = {
room = { "room", "bed", "sleep" },
rumor = { "rumor", "gossip", "news" },
},
topic_routes = {
room = "room_offer",
rumor = "rumors",
},
nodes = {
start = dialogue.node {
text = "Welcome to the Red Deer Inn. What do you need?",
options = {
dialogue.option { text = "A room", goto_ = "room_offer" },
dialogue.option { text = "Rumors", goto_ = "rumors" },
}
},
room_offer = dialogue.node {
text = "A room costs 15 gold coins.",
options = {
dialogue.option { text = "Accept", goto_ = "room_done" },
dialogue.option { text = "No thanks", goto_ = "bye" },
}
},
room_done = dialogue.node {
text = "The upstairs room is yours for the night.",
options = {
dialogue.option { text = "Thanks", goto_ = "bye" },
}
},
rumors = dialogue.node {
text = "They say the old mines to the north are inhabited again.",
options = {
dialogue.option { text = "Interesting", goto_ = "bye" },
}
},
bye = dialogue.node {
text = "Enjoy your stay.",
options = {}
}
}
})
Notes:
goto_is accepted becausegotois a Lua keywordtopicsdefines keyword groupstopic_routesmaps matched topic ids to destination nodesnodesis the real dialogue graph
Conditions And Effects
Options can define:
condition(ctx)to decide visibilityeffects(ctx)to mutate state before moving to the next node
dialogue.option {
text = "Accept",
condition = function(ctx)
return ctx:has_item("gold_coin", 15)
end,
effects = function(ctx)
ctx:remove_item("gold_coin", 15)
ctx:set_memory_flag("has_rented_room", true)
ctx:add_memory_number("rooms_rented", 1)
end,
goto_ = "room_done"
}
Context API
DialogueContext exposes both short-lived session state and persistent memory.
Actors:
ctx.speakerctx.listenerctx.conversation_idctx.node_id
Session state:
ctx:get_flag(key)ctx:set_flag(key, value)
Persistent memory:
ctx:get_memory_flag(key)ctx:set_memory_flag(key, value)ctx:get_memory_number(key)ctx:set_memory_number(key, value)ctx:add_memory_number(key, delta)ctx:get_memory_text(key)ctx:set_memory_text(key, value)
Speech and flow:
ctx:say(text)ctx:emote(text)ctx:yell(text)ctx:whisper(text)ctx:end_conversation()
Persistent Memory
Dialogue memory is typed and explicit. Each NPC stores entries keyed by the other mobile serial.
Stored data is limited to:
flags: Dictionary<string, bool>numbers: Dictionary<string, long>texts: Dictionary<string, string>last_nodelast_topiclast_interaction_utc
Runtime files live under:
moongate_data/runtime/dialogue_memory/<npc_serial>.json
Relationship To OpenAI Dialogue
dialogue and ai_dialogue are separate features:
dialogue- deterministic
- authored in Lua
- no OpenAI required
ai_dialogue- generative
- optional
- requires LLM configuration
You can run authored dialogue alone by binding only a conversation_id.
Authored Dialogue And OpenAI Together
Use common.npc_dialogue when you want deterministic dialogue first and OpenAI as fallback.
local npc_dialogue = require("common.npc_dialogue")
local DIALOGUE_CONFIG = {
conversation_id = "innkeeper",
prompt_file = "innkeeper.txt",
}
function innkeeper.on_spawn(npc_id, _ctx)
local npc = mobile.get(npc_id)
if npc == nil then
return
end
npc_dialogue.init(npc, DIALOGUE_CONFIG)
end
function innkeeper.on_speech(npc_id, speaker_id, text, _speech_type, _map_id, _x, _y, _z)
local npc = mobile.get(npc_id)
local speaker = mobile.get(speaker_id)
if npc == nil or speaker == nil then
return
end
if npc_dialogue.listener(npc, speaker, text, DIALOGUE_CONFIG) then
return
end
npc:say("Posso aiutarti in altro?")
end
Resolution order:
- active authored dialogue session and numeric option choice
- authored topic match
ai_dialogue.listener(...)fallback if configured
common.npc_dialogue is optional. If you do not want OpenAI at all, use only dialogue.init(...) and dialogue.listener(...).
Example Asset
The repository ships an example conversation here:
moongate_data/scripts/dialogs/innkeeper.lua
Use it as the reference pattern for new authored dialogues.