aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Metin2

Metin2 Dungeon Creation: Designing a Dungeon with Lua

Metin2 dungeon creation is the way to offer players a private map experience entered as a group, where monsters arrive in waves and a boss with rewards waits at the end. In this guide I walk through building a dungeon from scratch — entry conditions, teleporting players into the map, the wave system, the boss phase, a time limit and reward distribution — all with the Lua quest engine. The goal isn't copy-paste, but understanding the logic so you can design your own content.

How a dungeon works

In Metin2, a dungeon is really a map instance created specifically for a player or party. While normal maps are shared by everyone, a dungeon spawns as a separate copy for each group, so two different parties play the same dungeon without seeing each other. The layer that creates and manages this instance is the d. (dungeon) namespace in the quest engine.

  • Entry NPC or item — interacting with it triggers the dungeon.
  • Map index (map_index) — the number of the empty map the dungeon runs on; it must be defined in the index file.
  • Regen/spawn files — text files describing at which point monsters spawn.
  • Dungeon flags — counters specific to that dungeon (remaining monsters, wave number, start time) kept with d.setf/d.getf.

The key distinction: data tied to the player is stored with pc.setqf, while data tied to the dungeon instance is stored with d.setf. Since the wave count concerns the whole party it belongs to the dungeon, whereas personal progress is written to the player.

Entry conditions and teleporting in

A good dungeon is entered on fair terms. Typical checks: a minimum level, the party-leader requirement, a cooldown, and a key item if needed. The example below checks the conditions when a player talks to an entry NPC and teleports the party into a fresh dungeon instance:

quest dungeon_entry begin
    state start begin
        when 20355.chat."Enter the dungeon" begin
            if pc.get_level() < 40 then
                say("You must be at least level 40 to enter.")
                return
            end
            if party.is_party() and not party.is_leader() then
                say("Only the party leader can open the dungeon.")
                return
            end
            -- create a new map instance and teleport the party
            d.new_jump_party(NEW_DUNGEON_MAP_INDEX, 32000, 32000)
        end
    end
end

Here d.new_jump_party creates a fresh map instance and moves everyone in the party to the given coordinate. For a solo dungeon you can use d.new_jump_all instead. The coordinates are the map's local coordinates, not the global coordinates spanning the whole world.

Building the wave system

The heart of a dungeon is the wave system: a group of monsters spawns, the next wave arrives once they're all dead, and the boss appears last. The core logic has two parts — spawning (d.spawn_mob / d.regen_file) and counting (advance to the next step when no monsters remain). Here the dungeon-specific d.setf("wave", n) flag is a lifesaver.

-- runs when the map instance is created
when <map_index>.enter begin
    d.setf("wave", 1)
    d.notice("Wave 1 begins!")
    d.spawn_mob(101, 32000, 32000)
    d.spawn_mob(101, 32050, 32010)
    d.spawn_mob(101, 31980, 32030)
end

-- triggers whenever a monster in the dungeon dies
when kill begin
    if d.count_monster() > 0 then return end  -- monsters still alive
    local wave = d.getf("wave")
    if wave == 1 then
        d.setf("wave", 2)
        d.notice("Wave 2 incoming!")
        d.spawn_mob(102, 32000, 32000)
        d.spawn_mob(102, 32040, 32020)
    elseif wave == 2 then
        d.setf("wave", 3)
        d.notice("The boss has appeared!")
        d.set_unique("boss", d.spawn_mob(2493, 32000, 32000))
    end
end

d.count_monster() returns the number of monsters left on the map; when it hits zero you trigger the next wave. Naming the boss with d.set_unique matters so you can track it separately — we'll get to that next.

The boss phase and finishing the dungeon

The final wave usually spawns a single boss. When that boss dies the dungeon is completed successfully: rewards are granted, a short countdown is shown, and players are teleported out. Because we tagged the boss with d.set_unique, we can catch its specific death with d.is_unique_dead.

when kill with d.is_unique_dead("boss") begin
    d.notice("Dungeon cleared! Congratulations.")
    -- reward: yang and an item for the whole party
    d.kill_all()                      -- clear anything left
    d.spawn_mob(20356, 32000, 32000)  -- reward chest NPC
    d.exit_all_at_time(10)            -- teleport everyone out after 10s
end

Here d.kill_all() clears any leftover monsters, while d.exit_all_at_time teleports all players back after the seconds you specify. If you want to hand out the reward directly, loop over the party members when the boss dies and drop loot via pc.give_item2 or your mob_drop settings.

Time limit and the failure condition

Dungeons gain tension from time pressure. Set a countdown; when it expires the dungeon counts as failed and players are ejected. The quest engine's timer is ideal for this:

when <map_index>.enter begin
    -- start a 15-minute timer
    d.set_warp_location(NEW_DUNGEON_MAP_INDEX, 32000, 32000)
    server_timer("dungeon_timeout", 60 * 15, get_server_timer_arg())
end

when dungeon_timeout.server_timer begin
    d.notice("Time's up! Dungeon failed.")
    d.exit_all()
end

If you want to show the timer with a UI counter, you can send players a d.set_timer-style call or periodic warnings via notice. The important thing is to remember to cancel the timer once success (the boss death) occurs; otherwise a "time's up" message drops in after the reward has already been claimed.

Common mistakes

  • Wrong coordinate system: passing global coordinates drops the player outside the map or in the wrong spot. Always use local coordinates for in-dungeon teleports.
  • Counting error: if you decrement your own counter by hand instead of using d.count_monster(), the count drifts when a monster dies unexpectedly (e.g. fall damage). When possible, trust the value the engine counts.
  • Instance leak: if the dungeon instance isn't cleaned up when the party leaves or disbands, empty maps pile up. Both the boss death and the timeout must close their exit with d.exit_all.
  • Flag confusion: if you store the wave number with pc.setqf it's counted separately per player and the system breaks. The wave belongs to the dungeon → d.setf.

Frequently Asked Questions

Do I need a brand-new map for a dungeon?

You need a map index for the dungeon to run on, but it doesn't have to be a map drawn from scratch; you can set aside an existing empty area as the dungeon map. What matters is that the index is defined so it can be duplicated as an instance.

Can I build solo and group dungeons from the same code?

Yes. In the entry section, branch with party.is_party() and call d.new_jump_party for a group and d.new_jump_all for a solo player. The wave and boss logic stays identical for both; you can simply scale the monster count/difficulty to party size.

How do I protect rewards from abuse?

Grant the reward only inside the boss-death trigger, after marking the dungeon flag as "completed." Block a second payout in the same instance with a d.getf("done") check, and add an entry cooldown alongside the timer.

Want a fully balanced, exploit-proof dungeon for your server? I can build and test the whole thing — from entry conditions to wave and boss logic, reward and time systems. To discuss your project, get in touch with me.

Devamı için