Roblox
Belajar Luau 06: Proyek Mini Petualangan Dungeon
Menggabungkan variabel, if, loop, function, dan table dari pelajaran 01-05 menjadi game teks Petualangan Dungeon: toko, pertarungan bergiliran, ramuan, XP, dan naik level.

Saatnya menggabungkan semua materi pelajaran 01–05 (variabel, if, loop, function, return, dan table) menjadi satu game kecil: Petualangan Dungeon.
Jalankan di terminal:
luau 06-proyek-mini-dungeon.luau
Cara kerja game
- Pemain mulai dengan sedikit koin, lalu belanja di toko.
- Pemain masuk dungeon dan melawan musuh satu per satu.
- Setiap musuh yang kalah memberi koin dan XP.
- XP yang cukup membuat pemain naik level, sehingga darah dan serangannya naik.
- Game selesai saat semua musuh kalah atau pemain kalah.
Game ini berjalan otomatis tanpa menunggu ketikan, karena Luau di terminal tidak punya fitur input. Di Roblox, "input" datang dari tombol UI dan event, yang dipelajari nanti.
Cara belajar dari proyek ini: baca dari atas ke bawah. Setiap bagian diberi label materi yang dipakai, misalnya [P4] berarti pelajaran 04. Jalankan, lalu ubah angka-angka di bagian konfigurasi dan lihat bagaimana hasilnya berubah.
Angka acak yang bisa diulang
math.randomseed membuat angka acak selalu sama setiap dijalankan, supaya hasilnya mudah dibandingkan. Ganti angkanya (atau hapus baris ini) untuk mendapat pertarungan yang berbeda.
math.randomseed(42)
Konfigurasi [P5: dictionary dan array]
Game Roblox sungguhan juga memisahkan data seperti ini dari logika, supaya mudah diatur tanpa mengubah kode.
local TOKO = {
{nama = "Pedang Kayu", harga = 30, tipe = "serangan", nilai = 5},
{nama = "Perisai", harga = 40, tipe = "pertahanan", nilai = 4},
{nama = "Ramuan", harga = 15, tipe = "ramuan", nilai = 40},
}
local DAFTAR_MUSUH = {
{nama = "Slime", darah = 30, serangan = 6, koin = 10, xp = 40},
{nama = "Goblin", darah = 50, serangan = 10, koin = 20, xp = 70},
{nama = "Serigala", darah = 70, serangan = 14, koin = 30, xp = 90},
{nama = "Raja Orc", darah = 120, serangan = 18, koin = 100, xp = 200},
}
local XP_PER_LEVEL = 100
local BATAS_RAMUAN = 0.3 -- minum ramuan kalau darah di bawah 30%
Data pemain [P5: table bersarang]
local pemain = {
nama = "Budi",
level = 1,
xp = 0,
koin = 80,
darah = 100,
darahMaks = 100,
serangan = 8,
pertahanan = 0,
inventory = {}, -- array, diisi saat belanja
}
Function bantuan [P4]
local function garis()
print(string.rep("-", 40))
end
local function tampilkanStatus()
print(`[{pemain.nama}] Lv.{pemain.level} | Darah {pemain.darah}/{pemain.darahMaks}`
.. ` | Serang {pemain.serangan} | Tahan {pemain.pertahanan}`
.. ` | Koin {pemain.koin} | XP {pemain.xp}/{XP_PER_LEVEL}`)
end
-- [P3: loop] [P5: ipairs] Menghitung berapa ramuan yang dimiliki
local function jumlahItem(namaItem)
local jumlah = 0
for _, item in ipairs(pemain.inventory) do
if item == namaItem then
jumlah += 1
end
end
return jumlah
end
Toko [P2: if/else] [P4: multiple return]
local function cariItemToko(namaItem)
for _, item in ipairs(TOKO) do
if item.nama == namaItem then
return item
end
end
return nil
end
local function beli(namaItem)
local item = cariItemToko(namaItem)
if not item then
return false, `{namaItem} tidak dijual`
end
if pemain.koin < item.harga then
return false, `Koin kurang {item.harga - pemain.koin} untuk {namaItem}`
end
pemain.koin -= item.harga
-- Efek item tergantung tipenya
if item.tipe == "serangan" then
pemain.serangan += item.nilai
elseif item.tipe == "pertahanan" then
pemain.pertahanan += item.nilai
else
table.insert(pemain.inventory, item.nama) -- ramuan disimpan
end
return true, `Membeli {namaItem} (sisa koin {pemain.koin})`
end
Pertarungan [P3: while, break] [P4: function memanggil function]
Damage diberi sedikit unsur acak supaya pertarungan tidak selalu sama:
local function hitungDamage(serangan, pertahanan)
local acak = math.random(-2, 3)
local damage = serangan + acak - pertahanan
return math.max(damage, 1) -- minimal 1 damage
end
local function minumRamuanJikaPerlu()
local darahRendah = pemain.darah < pemain.darahMaks * BATAS_RAMUAN
if darahRendah and jumlahItem("Ramuan") > 0 then
local posisi = table.find(pemain.inventory, "Ramuan")
table.remove(pemain.inventory, posisi)
local pulih = cariItemToko("Ramuan").nilai
pemain.darah = math.min(pemain.darah + pulih, pemain.darahMaks)
print(` >> {pemain.nama} minum Ramuan! Darah jadi {pemain.darah}`)
end
end
local function cekNaikLevel()
while pemain.xp >= XP_PER_LEVEL do
pemain.xp -= XP_PER_LEVEL
pemain.level += 1
pemain.darahMaks += 20
pemain.darah = pemain.darahMaks -- naik level = darah penuh
pemain.serangan += 3
print(` *** NAIK LEVEL! Sekarang level {pemain.level} ***`)
end
end
Function utama pertarungan mengembalikan true kalau pemain menang. Perhatikan table.clone: tanpanya, darah musuh di DAFTAR_MUSUH ikut berkurang (jebakan referensi dari pelajaran 05).
local function bertarung(dataMusuh)
-- table.clone supaya data asli di DAFTAR_MUSUH tidak ikut berubah
local musuh = table.clone(dataMusuh)
print(`Seekor {musuh.nama} muncul! (darah {musuh.darah})`)
local ronde = 0
while true do
ronde += 1
-- Giliran pemain
local dmg = hitungDamage(pemain.serangan, 0)
musuh.darah -= dmg
print(` R{ronde}: {pemain.nama} menyerang {dmg} -> {musuh.nama} sisa {math.max(musuh.darah, 0)}`)
if musuh.darah <= 0 then
break
end
-- Giliran musuh
dmg = hitungDamage(musuh.serangan, pemain.pertahanan)
pemain.darah -= dmg
print(` R{ronde}: {musuh.nama} menyerang {dmg} -> {pemain.nama} sisa {math.max(pemain.darah, 0)}`)
if pemain.darah <= 0 then
return false
end
minumRamuanJikaPerlu()
end
-- Musuh kalah: beri hadiah
pemain.koin += musuh.koin
pemain.xp += musuh.xp
print(`{musuh.nama} kalah! +{musuh.koin} koin, +{musuh.xp} XP`)
cekNaikLevel()
return true
end
Jalannya game
print("=== PETUALANGAN DUNGEON ===")
tampilkanStatus()
garis()
-- Belanja: coba beli beberapa item. Ada yang akan gagal karena koin habis.
print("Masuk ke toko...")
local rencanaBelanja = {"Pedang Kayu", "Ramuan", "Ramuan", "Perisai", "Naga"}
for _, namaItem in ipairs(rencanaBelanja) do
local berhasil, pesan = beli(namaItem)
if berhasil then
print(" ✓ " .. pesan)
else
print(" ✗ " .. pesan)
end
end
tampilkanStatus()
garis()
-- Masuk dungeon: lawan musuh satu per satu
local musuhDikalahkan = 0
for i, musuh in ipairs(DAFTAR_MUSUH) do
print(`Ruang {i} dari {#DAFTAR_MUSUH}`)
local menang = bertarung(musuh)
if not menang then
print(`{pemain.nama} kalah di tangan {musuh.nama}...`)
break
end
musuhDikalahkan += 1
tampilkanStatus()
garis()
end
-- Hasil akhir
print("=== HASIL AKHIR ===")
if musuhDikalahkan == #DAFTAR_MUSUH then
print(`MENANG! {pemain.nama} menaklukkan dungeon!`)
else
print(`GAME OVER. Musuh dikalahkan: {musuhDikalahkan}/{#DAFTAR_MUSUH}`)
end
tampilkanStatus()
print(`Ramuan tersisa: {jumlahItem("Ramuan")}`)
Tantangan
Kembangkan game ini sendiri.
Mudah
- Tambahkan item baru di
TOKO, misalnya "Pedang Besi" (harga 60, serangan +10), lalu masukkan kerencanaBelanja. - Tambahkan musuh baru "Naga" yang sangat kuat di akhir
DAFTAR_MUSUH.
Sedang
- Serangan kritis: 20% kemungkinan damage pemain menjadi 2× lipat. Petunjuk:
math.random(1, 100) <= 20. - Setelah setiap musuh kalah, pemain otomatis mampir ke toko dan membeli Ramuan kalau koinnya cukup.
Sulit
- Simpan riwayat pertarungan di array: setiap musuh yang kalah dicatat sebagai
{nama = ..., ronde = ...}. Di akhir game, print musuh yang paling lama dikalahkan (ronde terbanyak). - Ganti seed (
math.randomseed) beberapa kali. Apakah pemain pernah kalah? Kalau tidak pernah, kenapa? Ubah konfigurasi (misalnya tambahkan musuh yang lebih kuat) sampai pemain kadang kalah, lalu cari seed yang membuat pemain kalah. Game yang seimbang itu menantang, tetapi tetap bisa dimenangkan.
Kunci jawaban
Coba kerjakan sendiri dulu sebelum membaca bagian ini. Di bawah ini hanya bagian yang berubah dari kode di atas.
Tantangan 1 dan 2: item dan musuh baru
Cukup tambah satu baris data. Logika beli() tidak perlu diubah, karena tipe "serangan" sudah ada.
local TOKO = {
-- ... item lama ...
{nama = "Pedang Besi", harga = 60, tipe = "serangan", nilai = 10},
}
local DAFTAR_MUSUH = {
-- ... musuh lama ...
{nama = "Naga", darah = 300, serangan = 40, koin = 300, xp = 400},
}
local rencanaBelanja = {"Pedang Kayu", "Pedang Besi", "Ramuan", "Ramuan", "Perisai", "Naga"}
Angka Naga dipilih lewat percobaan (lihat tantangan 6): dengan darah 300 dan serangan 40, pemain kalah di sekitar 1 dari 6 permainan, sehingga bos terakhir terasa menantang tetapi adil. Koin awal (80) tidak cukup untuk semua belanjaan, jadi sebagian pembelian sengaja gagal supaya terlihat pesan "Koin kurang".
Tantangan 3: serangan kritis
math.random(1, 100) menghasilkan angka 1–100 secara acak. Angka <= 20 muncul kira-kira 20 dari 100 kali, yaitu 20%.
local PELUANG_KRITIS = 20 -- persen
-- di dalam bertarung(), giliran pemain:
local dmg = hitungDamage(pemain.serangan, 0)
local kritis = math.random(1, 100) <= PELUANG_KRITIS
if kritis then
dmg *= 2
end
musuh.darah -= dmg
local tandaKritis = if kritis then " KRITIS!" else ""
print(` R{ronde}: {pemain.nama} menyerang {dmg}{tandaKritis} -> {musuh.nama} sisa {math.max(musuh.darah, 0)}`)
Tantangan 4 dan 5: mampir ke toko dan riwayat pertarungan
bertarung() sekarang mengembalikan jumlah ronde juga: return false, ronde saat kalah dan return true, ronde saat menang. Function beli() yang sudah ada dipakai ulang tanpa ditulis ulang.
local musuhDikalahkan = 0
local riwayat = {} -- array berisi {nama = ..., ronde = ...}
for i, musuh in ipairs(DAFTAR_MUSUH) do
print(`Ruang {i} dari {#DAFTAR_MUSUH}`)
local menang, ronde = bertarung(musuh)
if not menang then
print(`{pemain.nama} kalah di tangan {musuh.nama}...`)
break
end
musuhDikalahkan += 1
table.insert(riwayat, {nama = musuh.nama, ronde = ronde})
-- Mampir ke toko, hanya kalau masih ada musuh berikutnya
if i < #DAFTAR_MUSUH then
local berhasil, pesan = beli("Ramuan")
if berhasil then
print(" 🛒 Mampir ke toko: " .. pesan)
end
end
tampilkanStatus()
garis()
end
Setelah hasil akhir, cari musuh yang paling lama dikalahkan:
if #riwayat > 0 then
local terlama = riwayat[1]
for _, catatan in ipairs(riwayat) do
if catatan.ronde > terlama.ronde then
terlama = catatan
end
end
print(`Musuh terlama: {terlama.nama} ({terlama.ronde} ronde)`)
end
Tantangan 6: mencari seed yang membuat pemain kalah
Hasil percobaan dengan menjalankan seed 1 sampai 200 satu per satu:
| Versi | Hasil |
|---|---|
| Game asli dari materi (tanpa tantangan) | kalah 0 dari 300 seed |
| Naga darah 220, serangan 26 | kalah 0 dari 200 seed |
| Naga darah 300, serangan 36 | kalah 4 dari 200 seed |
| Naga darah 300, serangan 40 (dipakai) | kalah 35 dari 200 seed |
| Naga darah 320, serangan 40 | kalah 79 dari 200 seed |
Kesimpulannya:
- Di game asli, tidak ada seed yang membuat pemain kalah. Game-nya terlalu mudah, karena naik level memulihkan darah sampai penuh dan menambah serangan, sehingga pemain selalu lebih kuat dari musuh berikutnya. Mengganti seed saja tidak cukup; konfigurasinya yang harus diubah.
- Dengan Naga 300/40, seed 25 adalah seed pertama yang membuat pemain kalah. Coba ganti
math.randomseed(42)menjadimath.randomseed(25), lalu jalankan. - Perhatikan betapa sensitifnya angka: serangan Naga 36 → 40 mengubah kekalahan dari 2% menjadi 17%. Game designer sungguhan menyeimbangkan game dengan cara yang sama: ubah angka, uji banyak kali, lalu lihat datanya. Di Roblox, data itu datang dari Analytics (pelajaran 13), bukan dari mencoba seed.
Tantangan tambahan: untuk menguji banyak seed sekaligus tanpa mengetik ulang, ubah seluruh bagian "jalannya game" menjadi function mainkan(seed) yang me-return true/false, lalu panggil di dalam for seed = 1, 200.
Diskusi (0)
Kami menyimpan nama (opsional), komentar, dan hash teknis (bukan alamat IP asli) untuk mencegah spam. Cookie fungsional dipakai agar Anda dapat mengedit atau menghapus komentar sendiri dari browser ini. Untuk permintaan penghapusan, gunakan halaman kontak.
Belum ada komentar. Mulai diskusinya.