A Metin2 NPC dialogue begins the moment a player opens the chat window with an NPC, and when it is built well it feels like a small story. Showing a single line of text is easy; the real work is offering the player choices and branching the conversation based on the decision they make. In this article I walk through building multi-choice menus with select, yes/no prompts with confirm, and how to combine them into a clean, working Lua quest step by step.
The basic dialogue skeleton
Every NPC conversation opens with a chat trigger. You write the NPC's vnum, give the window a title, and lay out the text line by line with say functions.
quest villager_dialogue begin
state start begin
when 9003.chat."Talk to the villager" begin
say_title("Old Villager")
say("Welcome, traveller.")
say("")
say("What brings you here?")
end
end
end
Here 9003 is the NPC's vnum, say_title is the header of the dialogue window, and say is the body text. An empty say("") is used to leave a blank line. This block is one-way: the player only reads and clicks "Close". To add interaction we need the selection mechanisms.
Multi-choice menus with select
select shows the player a list of buttons and returns the index of the chosen option (starting at 1). You capture the returned value in a variable and branch on it with if / elseif.
when 9003.chat."Talk to the villager" begin
say_title("Old Villager")
say("How can I help you?")
say("")
local choice = select("Give me a quest", "Buy goods", "Tell your story", "Never mind")
if choice == 1 then
say("Then listen: clear out the wolves in the forest.")
elseif choice == 2 then
say("Sadly, I have nothing left to sell.")
elseif choice == 3 then
say("Years ago, this village was a very different place...")
else
say("Safe travels.")
end
end
A few things to watch: in practice select is comfortable up to about 6-7 options; any more and they won't fit the dialogue window. Making the last option a "Never mind" exit is a good habit so the player never gets stuck in the menu. Always store the returned value in a local variable, because if you call a second select the previous value is lost.
Yes/no confirmation with confirm
Before an irreversible action (deleting an item, teleporting, spending yang) you want to ask the player for confirmation. confirm("text") is exactly for this: it opens a "Yes / No" window for the player and returns true if they accept and false if they decline.
when 9003.chat."Teleport to the city" begin
say_title("Old Villager")
say("Shall I send you to the city?")
say("This costs 50,000 yang.")
say("")
if confirm("Do you want to teleport to the city?") then
if pc.get_money() >= 50000 then
pc.change_money(-50000)
pc.warp(469000, 964000)
else
say("You don't have enough yang.")
end
else
say("Maybe another time.")
end
end
confirm can be used directly inside an if condition because it returns a boolean. Some versions also offer confirm_with_button("question", "Yes label", "No label") for custom button text; however, since its availability depends on your source, stick with the standard confirm if you want to stay portable.
Chaining select and confirm together
The real power appears when you nest the two. The player first picks from a select menu, then commits to their final decision with confirm at the critical step. Below is a reward-chest NPC example:
when 9003.chat."Reward chest" begin
say_title("Mysterious Chest")
say("Choose a reward from inside, but be careful.")
say("There is no going back after you pick.")
say("")
local reward = select("Sword", "Armour", "Yang", "Later")
if reward == 4 then
say("The chest closed.")
return
end
if not confirm("Are you sure about your choice?") then
say("You paused for a moment over the chest.")
return
end
if reward == 1 then
pc.give_item2(11209, 1)
say("You earned a sharp sword!")
elseif reward == 2 then
pc.give_item2(11949, 1)
say("You earned sturdy armour!")
elseif reward == 3 then
pc.change_money(500000)
say("You earned 500,000 yang!")
end
end
Three techniques work together here: select determines the branch, an early return cleanly handles the "never mind" case, and confirm places a final safety gate before handing out the irreversible reward. Using return keeps the code out of deep if nesting and easy to read.
Making text dynamic
Static text gets dull fast. To weave the player's name, level, or item count into the text, use string concatenation inside say. In Lua the concatenation operator is two dots (..).
when 9003.chat."Greet" begin
say_title("Old Villager")
say("Hello "..pc.get_name()..", good to see you.")
say("You're level "..pc.get_level()..", you've progressed fast.")
end
This makes the same dialogue feel personal to every player. When you mix numeric values (pc.get_level()) into text, Lua converts them automatically, so you don't even need to call tostring().
Common mistakes and tips
- Forgetting to check the select result: the player can close the window with ESC; define a branch for every option plus a "never mind" exit.
- Writing confirm like a plain if and forgetting the parentheses:
confirm(...)always needs a text argument, don't leave it empty. - Very long say chains: after 4-5 lines, show the player a "Continue" button with
wait(); otherwise the text window overflows. - Special characters: save quest files in the encoding the server expects (usually UTF-8 or the relevant locale); the wrong encoding shows up as garbled characters in the dialogue.
- Don't forget to compile and reload the quest after a change; otherwise the game core keeps showing the old text.
Frequently Asked Questions
How many options does select support?
There is no hard technical limit, but in practice 6-7 options fit comfortably in the dialogue window. If you need more, split the options into categories: show the groups in the first menu, then open a second select based on the chosen group. This looks cleaner and stays readable.
Can I change the button text in the confirm window?
The standard confirm uses fixed "Yes/No" buttons. Some sources include an extended version that lets you set custom button text, but that depends on your version. If you want portability, the safest path is to phrase the actual question clearly with say and keep confirm simple.
Can I use the select result directly inside an if?
You can, but it isn't recommended. select shows the menu and returns the value; if you repeat the same call in two if branches, the menu opens twice. Instead, capture the result in a local variable, then compare that variable.
Stuck on NPC dialogues? If you need help with branching quest chains, multi-step dialogue systems, or menu design for your server, get in touch with me — let's build it together.