Примеры
Готовые куски, из которых собирается скрипт: фарм, расходники, баффы, маршруты, диалоги.
На этой странице
Готовые куски, из которых собирается рабочий скрипт. Всё, кроме отдельно отмеченного, работает и во встроенном редакторе, и в плагине-jar.
Числовые id умений и вещей в примерах — иллюстративные, подставляй свои.
Каркас цикла
Основа любого скрипта: перечитывать мир на каждой итерации и уступать управление через delay.
override suspend fun run(bot: L2Bot) {
bot.log("Старт: ${bot.user.name}")
while (true) {
val me = bot.user
if (me.dead) {
delay(3000)
continue
}
if (me.sitting) bot.stand()
// ... логика итерации ...
delay(500)
}
}Фарм: убить моба и подобрать свой дроп
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 возвращает null по таймауту — цикл просто пойдёт дальше и выберет цель заново.
Расходники по порогу
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) вернёт null, если вещи в инвентаре нет, — лишней команды не будет.
Поддержание баффов
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 — «не на перезарядке, не пассивное, не заблокировано». Хватит ли MP, оно не проверяет: если нужно, добавь bot.user.mp > ….
Хил самого раненого в группе
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)
}У участников группы нет template-id, поэтому цель задаётся строго по oid — через castSkillByOid.
Маршрут по геодате
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("Иду в $x / $y")
if (!bot.moveToByGeo(x, y, z, timeoutMs = 0)) {
bot.log("Не дошёл — беру следующую точку")
}
delay(500)
}
}
} finally {
bot.enableAutomation()
}
}timeoutMs = 0 снимает дедлайн: команда ждёт столько, сколько идётся. Зависнуть навсегда она не может — исчерпав попытки перестроить маршрут, вернёт false.
Автоматизация выключается на время ходьбы, иначе она параллельно потянет персонажа к своим целям. finally возвращает её обратно даже при остановке скрипта.
Диалог с 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
}Если цель уже выбрана — руками или предыдущей командой — диалог открывается без указания NPC:
bot.openDialog()
bot.log(bot.dialogText) // HTML открытого диалога
bot.selectDialog(1) // первый пунктОтвет на приглашение в группу
Вариант для редактора — дождаться окна-вопроса:
val ask = bot.waitEvent<ScriptEvent.ConfirmDialogReceived>(60_000)
if (ask != null) {
val accept = ask.sender == "MyLeader"
bot.confirmDialog(ask.msgId, ask.requestId, accept)
}Вариант для плагина-jar — постоянный фоновый обработчик:
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) {
// основная логика
delay(500)
}
}Смерть и возврат в город
if (bot.user.dead) {
bot.log("Умер — возвращаюсь в город")
bot.goHome(RestartType.TOWN)
delay(5000)
continue
}Перегруз
if (bot.user.weightPenalty > 0) {
bot.log("Перегруз — иду разгружаться")
bot.disableAutomation()
bot.moveToByGeo(townX, townY, townZ, timeoutMs = 0)
// ... продажа через диалог NPC ...
bot.enableAutomation()
}Обход персонажей аккаунта
restart() уводит на экран выбора, selectCharacter(slot) заводит в мир нужного. Слоты нумеруются с нуля, в том же порядке, что на экране выбора.
val slots = listOf(0, 1, 2)
for (slot in slots) {
if (!bot.restart()) {
bot.log("Не удалось выйти на экран выбора")
break
}
if (!bot.selectCharacter(slot)) {
bot.log("Слот $slot не зашёл — пропускаю")
continue
}
// Мир загружается не мгновенно: сразу после входа список NPC ещё пуст.
delay(3000)
bot.log("${bot.user.name}: рядом ${bot.npcs.size} NPC")
}После смены персонажа мир — другой: user, инвентарь, скиллы и окружение не имеют ничего общего с прежними. Всё, что запомнил до смены (oid-ы, цели, дропы), выбрось и перечитай bot.* заново.
Собранный скрипт целиком
Плагин-jar со всеми кусками вместе.
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 запущен на ${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)
}
}
}Не забудь зарегистрировать класс в META-INF/services/com.ikar.script.api.IkaScript — см. начало справочника.