У персонажей можно управлять автопилотом.
Поддерживаются включение/выключение, определение набора действий, их вероятностей и запуск определённого действия из этого определённого набора.

Эта статья будет идти от простого и более важного до менее востребованного, но более сложного и детализированного.
Если вы не являетесь программистом, то можно предположить, что вам будут полезны только первые 4 абзаца.

## Быстрый старт

Для начала можно просто "включить" автопилот без каких-либо дополнительных настроек, создав "стандартные действия" и установив их для нужного персонажа (или нескольких):

    std_actions = get_std_rpg_actions()

    character.set_auto(True)
    character_actions = character.set_actions(std_actions)
    character_actions.start('spawn')

Здесь `character` - RPG-персонаж (персонаж с вызванной `make_rpg`), которому устанавливается автопилот, а `character_actions` - копия действий, которая принадлежит именно этому персонажу.
Последняя строка запускает одно из стандартных действий - `spawn` (появление; в данном случае - в случайной локации).

## Полезные функции

Остановить текущее действие для персонажа `character`:
`character.get_actions().stop()`
При включённом автопилоте следующее обновление персонажа запустит случайное действие (см. далее).

Запустить действие `name` (не забывать про кавычки):
`character.get_actions().start('name')`

Запустить случайное действие:
`character.get_actions().random()`

Обновить действие, перед этим установив ему состояние `state`:
`character.get_actions().update(state = None)`
Если `state` не задано (по умолчанию), то оно не меняется.

Также автопилот можно выключить:
`character.set_auto(False)`

Следует указать, что при запуске нового действия (случайного или нет) или при выключении автопилота
текущее действие автоматически завершается (и сидящий персонаж встанет, а идущий остановится).

## Свойства

Используются при выполнении некоторых действий.

`interesting_places` - список названий интересных локаций, в которые персонаж будет заглядывать чаще, чем в другие.
Например, площадь и пляж: `std_actions.interesting_places = ['square', 'beach']`

`home` - имя локации домика для этого персонажа, либо (если такой локации нет) кортёж из имени локации и названия места в нём (рядом с какой-либо дверью), в котором этот персонаж будет пропадать во время "захода" в домик и появляться во время "выхода".
Примеры:
`character_actions.home = 'house'`
`character_actions.home = ('street', 'door')`

`friends` - список персонажей-друзей (не их имён, важно!) или ещё кого-либо, с кем данному персонажу будет логично время от времени о чём-то говорить. Если персонаж находится в списке "друзей" другого, то это не значит, что он сам обязан содержать его в своём списке, при необходимости этого можно не делать.
Пример: `dv.get_actions().friends = [us, mi]`

Как уже было сказано выше, функция `set_actions` создаёт копию переданных ей действий, присваивает её в действия персонажа и возвращает их в качестве своего результата. Свойства тоже копируются, поэтому в примере выше с `interesting_places` этот список будет доступен у всех персонажей, для которых установят эти `std_actions`.

Но так уж устроен Питон, что скопируется не сам список, а ссылка на него, поэтому при изменении этого списка у одного персонажа, он изменится и у всех остальных, и это следует учитывать.
Например, перед изменением можно делать копию:

    character_actions.interesting_places = list(character_actions.interesting_places)
    character_actions.interesting_places.remove('beach')
    character_actions.interesting_places.append('park')

Почему `set_actions` не делает сразу полную копию списка?
Потому что иногда именно такое поведение может быть удобным.

## Стандартные действия

Список стандартных действий:

- `spawn` - появление в локации из списка интересных мест, если он задан (см. предыдущий абзац), либо просто в случайной локации,
- `sit` - попытка посидеть некоторое время на ближайшем объекте-сиденье (если есть на текущей локации),
- `other_place` - переход в случайное другое место текущей локации,
- `near_location` - переход в случайное место соседней локации,
- `random_location` - переход в случайное место случайной локации,
- `interesting_place` - переход в случайное место локации из списка интересных (текущей локации, если список не задан),
- `look_around` - оглядеться по сторонам,
- `home` - пойти домой (если дом указан, см. предыдущий абзац),
- `to_friend` - пойти к персонажу из списка друзей (снова предыдущий абзац), одновременно посылая его к себе навстречу,
- `follow` - следовать за указанным персонажем (можно обновить с состояниями `rewind_to_min`/`rewind_to_max` для "перемотки" движения).

За каждое действие отвечает отдельная python-функция.
У стандартных действий эта функция имеет имя `rpg_action_%action_name%`.
Например, `rpg_action_spawn` или `rpg_action_to_friend`.

## Списки разрешённых и заблокированных действий

Хранятся в свойствах `allow` и `block` соответственно, содержат названия действий.
Заблокированное действие не может быть выбрано автоматически и не может быть запущено вручную.
Если список разрешённых действий не пуст, то запрещены все действия, которых в нём нет.
Копируются, как и другие свойства.

## Вероятности действий

Каждое действие имеет свою вероятность, с которой оно будет выбрано после выполнения предыдущего действия.
Также кроме обычной вероятности может быть задан диапазон из минимальной и максимальной, тогда вероятность будет новым случайным числом в этом диапазоне при каждом новом выборе.

Вероятность может быть нулевой, тогда такое действие никогда не будет выбрано автоматически (например, `spawn`).
Сумма вероятностей не обязана равняться `100`, это не проценты.

Стандартные действия имеют вероятности от `10` до `50` (у каждого по разному), но здесь не приводится точных цифр, т. к. они могут быть изменены в будущем, если будет решено, что это улучшит автопилот.
Посмотреть точные цифры на текущий момент можно в файле `Ren-Engine/rpy/rpg/actions.rpy` в функции `get_std_rpg_actions`.

Там же можно увидеть, как инициализируется объект `RpgActions`, и как можно обойтись без использования вышеупомянутой функции получения стандартных действий.

## Состояния действия

При запуске действие выполняется с состоянием (строкой) `start`.
Далее функция действия возвращает новое состояние, которое будет передано ей при следующем обновлении.
Если возвращается состояние `end`, то действие тут же вызывается снова с состоянием `end`, после чего оно считается завершённым, и происходит запуск случайного действия.

Почему при возврате состояния `end` функция вызывается с этим состоянием тут же, а не на следующем кадре?
Это сделано для удобства управления передвижением.
Метод `Character.move_to_place` пытается проложить дорогу до нужной точки, но если это невозможно, то двигает персонажа по прямой сквозь все препятствия (если начальная и конечная точка на 1 локации) или телепортирует в конечную точку (если она в другой локации).
Если это нежелательно, то в случае отсутствия пути для отмены передвижения следует вызвать этот же метод со значением `None`, чтобы остановить персонажа, после чего обычно следует завершение действия.
И ровно то же самое (остановка на месте) происходит и в состоянии принудительного завершения.
Поэтому, чтобы не дублировать код, состояние `end` обновляется сразу же.

Текущее состояние хранится в свойстве `state`.
Текущая функция - в свойстве `cur_action`.
Хранится именно функция, а не имя действия, т. к. это позволяет однократно запускать функцию без её регистрации в наборе действий.

## Пользовательские действия

Вы можете добавить свои функции для управления персонажами:
`actions.set_action("name", function, min_chance, max_chance = None)` Здесь:

- `actions` - RPG-действия, их можно получить, создав экземпляр класса `RpgActions` или из функции `get_std_rpg_actions`,
- `name` - имя устанавливаемого действия (строка),
- `function` - функция, управляющая поведением,
- `min_chance`, `max_chance` - диапазон вероятности запуска (об этом говорилось в абзаце "Вероятности действий").

Функция должна принимать 2 параметра:

- `character` - персонаж, которым нужно управлять,
- `state` - состояние действия на текущий момент (об этом говорилось в абзаце "Состояния действий").

Напоминание: получить RPG-действия персонажа `character` можно через `character.get_actions()`.
Через них можно запускать другие действия или хранить какие-то данные.

Также необходимо помнить, что:

- при `60` кадрах в секунду на обновление `1` кадра даётся `16` мс,
- кроме вашего кода выполняется куча другого кода,
- ваше действие может выполняться на `100` различных персонажах каждый кадр,
- наиболее сложно-вычислимые стадии действий иногда будут произходить сразу у нескольких персонажей в `1` кадре,
- у питона есть сборщик мусора, который тоже иногда будет вносить свой вклад в задержку обновления кадра. Поэтому вам нужно максимально экономить ресурсы компьютера.

Например, прокладывание пути (тяжёлая операция) в большинстве случаев достаточно сделать `1` раз (на старте), в то время как бОльшую часть времени будет происходить дешёвая по времени проверка вида "Ещё идём? Да - продолжаем идти. Нет - завершение".
Если же путь нужно время от времени обновлять (например, если цель пути - двигающийся персонаж), то делать это стоит не каждый кадр, а раз в секунду, а если локации персонажа и цели разные (т. е. расстояние велико) - раз в `3` секунды (цифры даны в качестве простого и понятного примера).

## Работа со временем

Система сохранения и загрузки игры накладывает свой отпечаток на работу со временем.

Нельзя полагаться на то, что следующий кадр будет через примерно `0.016` секунды, или что текущее время + `3` секунды не будет давно прошедшим для уже следующего кадра.
Вполне возможно, что в текущем кадре будет сделано сохранение, которое будет загружено через минуту или через месяц (в любом случае вы рискуете получить неадекватный результат).

Поэтому рекомендуется вместо `time.time()` использовать специальную функцию `get_game_time()`, которая учитывает лишь нужное время.

Может оказаться полезной вспомогательная функция для поиска случайного места, свободного для передвижения:
`rpg_random_free_point(location_names)`
Где `location_names` - список локаций (`None` недопустим).
Возвращает кортёж `(location_name, x, y)` или `None` (если некоторое число случайных проверок не дали результата).

Далее ->
<- Назад