Examples
Working pieces to assemble a script from: farming, consumables, buffs, routes, dialogs.
On this page
Ready-made pieces you can assemble a working script from. Unless noted otherwise, everything works both in the built-in editor and in a jar plugin.
Skill and item ids in the examples are illustrative — substitute your own.
The loop skeleton
The backbone of any script: re-read the world every iteration and yield control with delay.
override suspend fun run(bot: L2Bot) {
bot.log("Started: ${bot.user.name}")
while (true) {
val me = bot.user
if (me.dead) {
delay(3000)
continue
}
if (me.sitting) bot.stand()
// ... iteration logic ...
delay(500)
}
}Farming: kill a mob and pick up your drop
override suspend fun run(bot: L2Bot) {
while (true) {
val mob = bot.npcs.nearest { it.attackable && !it.dead && it.distToSelf <= 1500 }
if (mob == null) {
delay(1000)
continue
}
if (mob.distToSelf > 300) bot.moveToByGeo(mob, timeoutMs = 5000)
bot.attack(mob)
bot.waitEvent<ScriptEvent.Died>(20_000) { it.oid == mob.oid }
val drop = bot.drops.nearest { it.isMine && it.distToSelf <= 250 }
if (drop != null) {
if (drop.distToSelf > 60) bot.moveToByGeo(drop, timeoutMs = 4000)
bot.pickup(drop)
}
delay(300)
}
}waitEvent returns null on timeout — the loop simply moves on and picks a target again.
Consumables by threshold
private suspend fun useConsumables(bot: L2Bot) {
val me = bot.user
if (me.hpPercent < 0.5f && me.item(1061) != null) bot.useItem(1061)
if (me.mpPercent < 0.3f && me.item(728) != null) bot.useItem(728)
}me.item(itemId) returns null when the item isn't in the inventory, so no pointless command goes out.
Keeping buffs up
private val myBuffs = listOf(1204, 1085, 1068)
private suspend fun keepBuffs(bot: L2Bot) {
if (bot.user.sitting || bot.user.isCasting) return
for (id in myBuffs) {
if (bot.user.hasBuff(id)) continue
val skill = bot.user.skill(id) ?: continue
if (!skill.ready) continue
bot.castSkill(id)
delay(1200)
}
}skill.ready means "off cooldown, not passive, not blocked". It does not check whether you have the MP — add bot.user.mp > … if you need that.
Healing the most wounded party member
private suspend fun healParty(bot: L2Bot, healSkillId: Int, threshold: Float) {
if (bot.user.isCasting || bot.user.sitting) return
val skill = bot.user.skill(healSkillId) ?: return
if (!skill.ready) return
val hurt = bot.party
.filter { it.hpPercent < threshold }
.minByOrNull { it.hpPercent } ?: return
bot.castSkillByOid(healSkillId, hurt.oid)
}Party members have no template id, so the target is given strictly by oid — through castSkillByOid.
A geodata route
override suspend fun run(bot: L2Bot) {
val route = listOf(
Triple(82500, 148000, -3470),
Triple(83700, 148900, -3400),
Triple(84600, 147500, -3390),
)
bot.disableAutomation()
try {
while (true) {
for ((x, y, z) in route) {
bot.log("Heading to $x / $y")
if (!bot.moveToByGeo(x, y, z, timeoutMs = 0)) {
bot.log("Didn't make it — taking the next point")
}
delay(500)
}
}
} finally {
bot.enableAutomation()
}
}timeoutMs = 0 removes the deadline: the command waits as long as the walk takes. It still cannot hang forever — once replanning attempts run out it returns false.
Automation is switched off for the walk, otherwise it would pull the character towards its own goals at the same time. finally restores it even if the script is stopped.
Talking to an NPC
private suspend fun talk(bot: L2Bot, npcId: Int, option: String): Boolean {
if (!bot.openDialog(npcId)) return false
delay(300)
if (!bot.selectDialog(option)) return false
delay(300)
return true
}If the target is already selected — by hand or by a previous command — the dialog opens without naming an NPC:
bot.openDialog()
bot.log(bot.dialogText) // HTML of the open dialog
bot.selectDialog(1) // first entryAnswering a party invitation
Editor-friendly version — wait for the confirmation window:
val ask = bot.waitEvent<ScriptEvent.ConfirmDialogReceived>(60_000)
if (ask != null) {
val accept = ask.sender == "MyLeader"
bot.confirmDialog(ask.msgId, ask.requestId, accept)
}Jar plugin version — a permanent background handler:
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.collect
override suspend fun run(bot: L2Bot) = coroutineScope {
launch {
bot.events.filterIsInstance<ScriptEvent.ConfirmDialogReceived>().collect { ask ->
bot.confirmDialog(ask.msgId, ask.requestId, ask.sender == "MyLeader")
}
}
while (true) {
// main logic
delay(500)
}
}Death and going back to town
if (bot.user.dead) {
bot.log("Died — returning to town")
bot.goHome(RestartType.TOWN)
delay(5000)
continue
}Overweight
if (bot.user.weightPenalty > 0) {
bot.log("Overweight — going to unload")
bot.disableAutomation()
bot.moveToByGeo(townX, townY, townZ, timeoutMs = 0)
// ... sell through an NPC dialog ...
bot.enableAutomation()
}Walking the characters on an account
restart() goes to the selection screen, selectCharacter(slot) brings the chosen one into the world. Slots are zero-based, in the same order as on the screen.
val slots = listOf(0, 1, 2)
for (slot in slots) {
if (!bot.restart()) {
bot.log("Could not reach the selection screen")
break
}
if (!bot.selectCharacter(slot)) {
bot.log("Slot $slot did not enter — skipping")
continue
}
// The world does not load instantly: right after entering, the NPC list is still empty.
delay(3000)
bot.log("${bot.user.name}: ${bot.npcs.size} NPCs nearby")
}After a switch the world is a different character: user, the inventory, the skills and the surroundings have nothing in common with the previous ones. Drop everything remembered before the switch (oids, targets, drops) and re-read bot.*.
The whole script
A jar plugin with all the pieces put together.
package com.example.myscript
import com.ikar.script.api.IkaScript
import com.ikar.script.api.L2Bot
import com.ikar.script.api.ScriptMeta
import com.ikar.script.api.model.nearest
import com.ikar.script.api.waitEvent
import com.ikar.script.protocol.ScriptEvent
import kotlinx.coroutines.delay
class Farmer : IkaScript {
override val meta = ScriptMeta(id = "farmer", name = "Farmer")
private val myBuffs = listOf(1204, 1085, 1068)
override suspend fun run(bot: L2Bot) {
bot.log("Farmer started on ${bot.user.name}")
while (true) {
val me = bot.user
if (me.dead) {
bot.goHome()
delay(5000)
continue
}
if (me.sitting) bot.stand()
useConsumables(bot)
keepBuffs(bot)
val mob = bot.npcs.nearest { it.attackable && !it.dead && it.distToSelf <= 1500 }
if (mob == null) {
delay(1000)
continue
}
if (mob.distToSelf > 300) bot.moveToByGeo(mob, timeoutMs = 5000)
bot.attack(mob)
bot.waitEvent<ScriptEvent.Died>(20_000) { it.oid == mob.oid }
val drop = bot.drops.nearest { it.isMine && it.distToSelf <= 250 }
if (drop != null) {
if (drop.distToSelf > 60) bot.moveToByGeo(drop, timeoutMs = 4000)
bot.pickup(drop)
}
delay(300)
}
}
override fun onStop() {}
private suspend fun useConsumables(bot: L2Bot) {
val me = bot.user
if (me.hpPercent < 0.5f && me.item(1061) != null) bot.useItem(1061)
if (me.mpPercent < 0.3f && me.item(728) != null) bot.useItem(728)
}
private suspend fun keepBuffs(bot: L2Bot) {
if (bot.user.isCasting) return
for (id in myBuffs) {
if (bot.user.hasBuff(id)) continue
val skill = bot.user.skill(id) ?: continue
if (!skill.ready) continue
bot.castSkill(id)
delay(1200)
}
}
}Don't forget to register the class in META-INF/services/com.ikar.script.api.IkaScript — see the start of the reference.