> For the complete documentation index, see [llms.txt](https://docs.nursultan.fun/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nursultan.fun/scripting-api-ru/primery/keystrokes.md).

# Keystrokes

Показывает на экране, какие клавиши движения нажаты, плюс CPS мыши. Хороший пример того, как собрать HUD: состояние копится в обработчике ввода, а кадр только рисует.

```kotlin
name("Keystrokes")
description("Показывает нажатые клавиши движения")

key(Key.K)

val x = slider("X", 10, 0, 400)
val y = slider("Y", 100, 0, 400)
val size = slider("Размер", 22, 12, 40)
val accent = colorPicker("Цвет нажатия", Colors.rgba(90, 200, 255, 255))
val showCps = checkBox("Показывать CPS", true)

var forward = false
var backward = false
var left = false
var right = false
var jump = false

val clicks = ArrayDeque<Long>()

onEnable {
    clicks.clear()
}

on<MoveInputEvent> { e ->
    forward = e.forward()
    backward = e.backward()
    left = e.left()
    right = e.right()
    jump = e.jump()
}

on<KeyEvent> { e ->
    if (e.pressed() && e.matches(Key.MOUSE_1)) {
        clicks.addLast(client.millis())
    }
}

fun cps(): Int {
    val now = client.millis()
    while (clicks.isNotEmpty() && now - clicks.first() > 1000L) {
        clicks.removeFirst()
    }
    return clicks.size
}

on<Render2DEvent> { e ->
    whenInGame {
        val r = e.render()
        val cell = size.value()
        val gap = 2f
        val originX = x.value()
        val originY = y.value()

        fun keyBox(column: Int, row: Int, label: String, pressed: Boolean) {
            val boxX = originX + column * (cell + gap)
            val boxY = originY + row * (cell + gap)
            val background = if (pressed) accent.value() else Colors.rgba(0, 0, 0, 130)
            val text = if (pressed) Colors.BLACK else Colors.WHITE
            r.roundedRect(boxX, boxY, cell, cell, 4f, background)
            val width = r.textWidth(label, cell * 0.45f)
            r.text(label, boxX + (cell - width) / 2f, boxY + cell * 0.3f, cell * 0.45f, text)
        }

        keyBox(1, 0, "W", forward)
        keyBox(0, 1, "A", left)
        keyBox(1, 1, "S", backward)
        keyBox(2, 1, "D", right)

        val spaceY = originY + 2 * (cell + gap)
        val spaceWidth = cell * 3 + gap * 2
        r.roundedRect(originX, spaceY, spaceWidth, cell * 0.6f, 4f,
            if (jump) accent.value() else Colors.rgba(0, 0, 0, 130))

        if (showCps.value()) {
            val text = cps().toString() + " cps"
            r.text(text, originX, spaceY + cell * 0.8f, cell * 0.4f, Colors.WHITE)
        }
    }
}
```

## Что тут стоит заметить

**Состояние копится вне кадра.** `MoveInputEvent` приходит раз в тик, а кадров — десятки в секунду. Если читать ввод прямо в `Render2DEvent`, ничего не выйдет: там его просто нет.

**CPS считается по очереди меток времени.** Складываем время каждого клика, выбрасываем всё старше секунды — сколько осталось, столько и кликов в секунду.

**Настройки читаются в кадре, а не сохраняются в переменные.** Игрок может подвинуть слайдер в любой момент, и HUD должен сразу поехать за ним.

**Локальная функция вместо копипасты.** `keyBox` объявлена внутри обработчика — так она видит и `r`, и размеры, и не приходится тащить их аргументами.
