Больше мешочков для бога мешочков
Больше мешочков для бога мешочков

Привет, Хабр!

После долгого перерыва — снова Сфера. Прошлые статьи (раз, два) были про то, как войти в игру и в ней остаться на всю жизнь. Гулять по миру, конечно, интересно, но быстро надоедает: делать в нем нечего, монстров и NPC нет, даже в озере утонуть не выйдет. Начнем нашу дорогу в темное средневековье там же, где начинается сама игра — в стартовом данже.

Стартовый данж

Процесс первого входа в игру новых персонажем выглядит так:

  1. Персонаж оказывается в пустоте (координата y >= 3000)

  2. Сервер отправляет пакет с информацией об инстансе

  3. Клиент загружает геометрию, модели и вспомогательные объекты

  4. Сервер отправляет пакет телепортации

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

Первые несколько мгновений после загрузки
Первые несколько мгновений после загрузки

Информация об инстансе

В стартовом данже: 2 NPC, монстр, оружие (короткий меч I ранга), телепорт, невидимые маркеры обучающих подсказок и случайное количество объектов (сундуки и другие контейнеры). Про генерацию объектов будет отдельная статья, там много нового и неизведанного. Пока что для примера возьмем первый попавшийся пакет со стартовым данжем в координатах (-1100, 4500, 1900). Кроме типа геометрии и координат самого инстанса (включая поворот), сервер также передает:

  1. Все объекты:

    1. Уникальный ID объекта

    2. ID в игровой базе данных (params/group*.cfg)

    3. Координаты

  2. Монстры, NPC:

    1. Уровень

    2. Текущее и максимальное здоровье

    3. Имя (NPC и именные монстры), фамилия (NPC) из списка в language/_rnms.txt

  3. Предметы:

    1. Тип (например, меч/броня/металл/порошок)

    2. Суффикс (например, огня/стихий/урона/спешки)

    3. ID контейнера (для предметов в мешках и сундуках)

    4. Текущая прочность

Пакет со стартовым данжем
public static byte[] LoadNewPlayerDungeon => new byte[]
{
   0xBF, 0x00, 0x2C, 0x01, 0x00, 0x06, 0x7A, 0x2C, 0x0C, 0x10, 0x80, 0x2F, 0x81, 0x1F, 0x01, 0x0B, 0xE2, 0xE0,
   0x03, 0x20, 0xA1, 0x4B, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x64, 0x91, 0x01, 0xA0,
   0x00, 0x03, 0x9A, 0xFE, 0x00, 0x85, 0x09, 0x00, 0xF8, 0xF9, 0xAF, 0x00, 0x06, 0x3E, 0x00, 0xC0, 0x44, 0x62,
   0x00, 0x50, 0xC6, 0x22, 0x00, 0xC0, 0x76, 0x22, 0x00, 0x44, 0x16, 0x19, 0x00, 0x0A, 0x2F, 0x80, 0x9D, 0xF5,
   0x0B, 0x60, 0xE1, 0x33, 0x7C, 0xA2, 0x83, 0x8D, 0xC4, 0x36, 0xA8, 0x8C, 0x45, 0xF0, 0xFB, 0xF1, 0x44, 0xC1,
   0x88, 0x2C, 0x32, 0x00, 0x14, 0x5E, 0x40, 0x20, 0xA0, 0xF0, 0x0C, 0xA2, 0x83, 0x8D, 0xC4, 0x36, 0xA8, 0x8C,
   0x45, 0xF0, 0xFB, 0xF1, 0x44, 0x85, 0x6F, 0xD0, 0xED, 0x2C, 0xFE, 0x55, 0x8F, 0x48, 0x06, 0x56, 0x8F, 0x48,
   0x06, 0x06, 0xF8, 0x21, 0x2C, 0x40, 0x0D, 0x3E, 0x9F, 0x10, 0x45, 0x62, 0x97, 0x56, 0xC6, 0x22, 0x41, 0xA4,
   0x76, 0xA2, 0xE5, 0x50, 0x14, 0x00, 0x14, 0x43, 0x3A, 0x29, 0xDE, 0x07, 0x85, 0x17, 0x00, 0x00, 0xF8, 0x25,
   0x2C, 0xA0, 0x1F, 0x3E, 0x19, 0xA1, 0x46, 0x62, 0xB0, 0x53, 0xC6, 0x22, 0x49, 0xC2, 0x76, 0x22, 0x80, 0x44,
   0x16, 0x19, 0x89, 0x0A, 0x59, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0x0F, 0xC1, 0x00, 0x2C, 0x01, 0x00, 0x06, 0x7A,
   0x15, 0x0B, 0x24, 0x83, 0xCF, 0x38, 0xA3, 0x91, 0xB8, 0x94, 0x95, 0xB1, 0xC8, 0x13, 0x31, 0x9E, 0x68, 0x0C,
   0x14, 0x05, 0x00, 0xC5, 0x50, 0xEA, 0x80, 0x00, 0xC0, 0xCF, 0x62, 0x81, 0x3F, 0xF0, 0x01, 0xCD, 0x25, 0x12,
   0xAB, 0xA2, 0x32, 0x16, 0x09, 0x95, 0xCB, 0x13, 0x01, 0x24, 0xB2, 0xC8, 0x00, 0x50, 0xC8, 0x02, 0x80, 0xFF,
   0xFF, 0xFF, 0xFF, 0xC2, 0x13, 0x2C, 0x1C, 0x00, 0x04, 0xFC, 0x0C, 0x58, 0x00, 0x16, 0x1F, 0x00, 0x09, 0x5D,
   0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x22, 0x8B, 0x0C, 0x00, 0x05, 0x18, 0xD0, 0xF4,
   0x07, 0x28, 0x64, 0x01, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x7F, 0x85, 0xCF, 0xF0, 0x01, 0x00, 0x26, 0x12,
   0x03, 0x80, 0x32, 0x16, 0x01, 0x00, 0xB6, 0x13, 0x01, 0x20, 0xB2, 0xC8, 0x00, 0x50, 0x78, 0x81, 0x00, 0x81,
   0xC2, 0x33, 0x64, 0x6D, 0xCF, 0x15, 0xEB, 0x82, 0x26, 0x12, 0x25, 0x7C, 0xD0, 0x15, 0x17, 0xBE, 0x01, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0xF0, 0x0C, 0x19, 0xD9, 0x76, 0xC5,
   0xDB, 0xB4, 0x89, 0x44, 0x0D, 0x7E, 0x72, 0xC5, 0x85, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC8, 0x00, 0x2C, 0x01, 0x00, 0x06, 0x7A, 0xFF, 0x2B, 0x7C, 0x46, 0xE1,
   0x19, 0x08, 0xC0, 0xE6, 0x8A, 0xF5, 0x41, 0x13, 0x89, 0xFE, 0x18, 0xE2, 0x8A, 0x0B, 0xDF, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x60, 0xE1, 0x33, 0x7C, 0x00, 0x80,
   0x89, 0xC4, 0x00, 0xA0, 0x8C, 0x45, 0x00, 0x80, 0xED, 0x44, 0x00, 0x88, 0x2C, 0x32, 0x00, 0x14, 0x5E, 0x40,
   0x40, 0xA0, 0xF0, 0x0C, 0x73, 0x7F, 0x75, 0xC5, 0x14, 0xA1, 0x89, 0x44, 0x20, 0x82, 0x71, 0xC5, 0x85, 0x6F,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xE5, 0x3E, 0xF0, 0x19,
   0x3E, 0x00, 0xC0, 0x44, 0x62, 0x00, 0x50, 0xC6, 0x22, 0x00, 0xC0, 0x76, 0x22, 0x00, 0x44, 0x16, 0x19, 0x00,
   0x0A, 0x2F, 0x30, 0x20, 0x50, 0x78, 0x06, 0x99, 0x06, 0xBA, 0xE2, 0x83, 0xDA, 0x44, 0xA2, 0x2B, 0xC1, 0xB9,
   0xE2, 0xC2, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x74,
   0x1F, 0xF8, 0x01, 0x9F, 0xB9, 0x5C, 0x22, 0x31, 0xDE, 0x2A, 0x63, 0x11, 0xFB, 0xDE, 0x3C, 0x11, 0x00, 0x22,
   0x8B, 0x0C, 0x00, 0x3F, 0x06, 0x16, 0x90, 0xC0, 0x27, 0xC3, 0x96, 0x48, 0x9C, 0xBF, 0xC9, 0x58, 0xE4, 0xDB,
   0xD7, 0x4E, 0x04, 0x80, 0xC8, 0x22, 0x03, 0x00, 0xC9, 0x00, 0x2C, 0x01, 0x00, 0x06, 0x7A, 0x0C, 0x2C, 0x20,
   0x41, 0xE1, 0x05, 0x02, 0x00, 0x7E, 0x02, 0x2C, 0x20, 0x81, 0xCF, 0x1B, 0x9A, 0x91, 0xD8, 0xF2, 0x92, 0xB1,
   0x08, 0x0E, 0xB0, 0x9D, 0x08, 0x00, 0x91, 0x45, 0x06, 0x80, 0xC2, 0x0B, 0x08, 0x00, 0xFC, 0x06, 0x58, 0x40,
   0x02, 0x9F, 0x58, 0xC7, 0x23, 0x31, 0xAE, 0x28, 0x63, 0x91, 0x1E, 0x86, 0x3B, 0x11, 0x00, 0x22, 0x8B, 0x0C,
   0x00, 0x85, 0x17, 0x18, 0x00, 0xF8, 0x09, 0x3F, 0x80, 0x04, 0x3E, 0x1F, 0xD3, 0x46, 0x62, 0x15, 0x4F, 0xC6,
   0x22, 0xEE, 0xA8, 0x78, 0x22, 0x00, 0x44, 0x16, 0x19, 0x00, 0x0A, 0x2F, 0x40, 0x00, 0xF0, 0x2B, 0x53, 0x00,
   0x09, 0x7C, 0xE8, 0xAF, 0x8B, 0xC4, 0x46, 0x96, 0x8C, 0x45, 0xD2, 0x42, 0xF1, 0x44, 0x00, 0x88, 0x2C, 0x32,
   0x00, 0x14, 0x5E, 0xA0, 0x00, 0xE0, 0xA7, 0xBF, 0x02, 0x12, 0xF8, 0x38, 0xE5, 0x12, 0x89, 0x99, 0x41, 0x19,
   0x8B, 0x74, 0x4F, 0xE6, 0x89, 0x00, 0x10, 0x59, 0x64, 0x00, 0x28, 0xBC, 0xC0, 0x01, 0xC0, 0x6F, 0x7F, 0x05,
   0x24, 0xF0, 0x79, 0xE9, 0x25, 0x12, 0xB3, 0x61, 0x32, 0x16, 0x41, 0x82, 0xC6, 0x13, 0x01, 0x20, 0xB2, 0xC8,
   0x00, 0x50, 0x78, 0x01, 0x03, 0x80, 0x9F, 0xDE, 0x02, 0xAE, 0xE1, 0x03, 0x20, 0xA1, 0x4B, 0x02, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x64, 0x91, 0x01, 0x00, 0x1B, 0x00, 0x2C, 0x01, 0x00, 0x06, 0x7A,
   0x7A, 0x0B, 0xB8, 0x46, 0x01, 0x06, 0x34, 0xFD, 0x01, 0x0A, 0x13, 0x10, 0x50, 0xC8, 0x02, 0x80, 0xFF, 0xFF,
   0xFF, 0x7F, 0x2D, 0x00, 0x2C, 0x01, 0x00, 0x6D, 0xF7, 0x8A, 0x2C, 0xDB, 0xE1, 0x40, 0x0F, 0x61, 0x01, 0x6A,
   0x10, 0x98, 0xF9, 0xF4, 0x35, 0xFE, 0xF2, 0x2F, 0x61, 0x01, 0xFD, 0x10, 0x00, 0x6D, 0xFE, 0xD7, 0x1F, 0xC0,
   0xCF, 0x62, 0x81, 0x3F, 0x10, 0x54, 0x7E, 0xFE, 0xD9, 0x09, 0x00
};

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

Телепортация

Пакет телепортации очень простой и логичный:

{
   0x1F, 0x00, 0x2C, 0x01, 0x00, 0x00, 0x04, MajorByte(ID), MinorByte(ID), 0x08, 0x40, 0xE3, 0x01,
   (byte)x_1, (byte)x_2, (byte)x_3, (byte)x_4, (byte)y_1, (byte)y_2, (byte)y_3, (byte)y_4, (byte)z_1,
   (byte)z_2, (byte)z_3, (byte)z_4, (byte)t_1, (byte)t_2, (byte)t_3, (byte)t_4, (byte)t_5, 0x00
};
  1. ID персонажа

  2. Новые координаты и поворот

Точнее, в другой игре был бы простой и логичный. Такой пакет подойдет для обычного перемещения персонажа (например, жетоном телепортации или способностью), но не для стартового данжа. Вот правильный:

{
   0xAB, 0x00, 0x2C, 0x01, 0x00, 0x00, 0x04, MajorByte(ID), MinorByte(ID), 0x08, 0x40, 0xE3, 0x01,
   (byte)x_1, (byte)x_2, (byte)x_3, (byte)x_4, (byte)y_1, (byte)y_2, (byte)y_3, (byte)y_4, (byte)z_1,
   (byte)z_2, (byte)z_3, (byte)z_4, (byte)t_1, (byte)t_2, (byte)t_3, (byte)t_4, (byte)t_5, 0x20, 0x08,
   0x39, 0xED, 0xA8, 0x00, 0xC8, 0x00, 0x00, 0x00, 0x0B, 0x40, 0xE7, 0x45, 0x20, 0xF7, 0x42, 0x10, 0x79,
   0x31, 0x88, 0xBC, 0x20, 0x24, 0x5B, 0x14, 0x22, 0x2F, 0x0C, 0x60, 0x71, 0x00, 0x0B, 0x04, 0x58, 0x24,
   0xC0, 0x42, 0x01, 0x16, 0x0B, 0xB0, 0x60, 0x80, 0x45, 0x03, 0x2C, 0x1C, 0x64, 0xF1, 0x20, 0x0B, 0x08,
   0x58, 0x44, 0xC0, 0x42, 0x02, 0x16, 0x13, 0xB0, 0xA0, 0x80, 0x45, 0x05, 0x2C, 0x2C, 0x60, 0x71, 0x01,
   0x0B, 0x4C, 0xE4, 0x45, 0x26, 0xF2, 0x42, 0x13, 0x79, 0xB1, 0x01, 0x0B, 0x0E, 0x58, 0x74, 0xC0, 0xC2,
   0x03, 0x16, 0x1F, 0xB0, 0x00, 0x81, 0x45, 0x08, 0x2C, 0x44, 0x60, 0x31, 0x22, 0x0B, 0x12, 0x59, 0x94,
   0xC0, 0xC2, 0x04, 0x16, 0x27, 0xB6, 0x40, 0x81, 0x45, 0x0A, 0x2C, 0x54, 0x60, 0xB1, 0x0A, 0xB1, 0x60,
   0xC1, 0x45, 0x0B, 0x2E, 0x5C, 0x60, 0x31, 0x03, 0x0B, 0x1A, 0x58, 0xD4, 0xC0, 0xC2, 0x06, 0x1B, 0x12,
   0x02, 0xF6, 0x02
};
  1. ID персонажа

  2. Новые координаты и поворот

  3. Вся информаций о персонаже с небольшими изменениями:

    1. Здоровье (с примененным эффектом от сытости, 100/110 вместо 100/100)

    2. Деньги (иногда 10, иногда 0, как повезет)

Почему нельзя было сразу создать персонажа с нужными параметрами и почему количество денег меняется? Потому что Сфера — мир страданий.

Серверная логика

Клиент успешно попал в стартовый данж, но геймплейной логики у нас нет никакой. Как минимум, хочется подобрать меч, убить монстра, собрать с него лут, продать лут NPC и телепортироваться на материк. Получать от монстра урон и убегать от него по данжу нравится не всем, но тоже нужно. В Сфере перемещение объектов и “физика” считаются на сервере, которому в первую очередь надо знать геометрию пространства вокруг игрока.

Геометрия

Вся информация о геометрии общих локаций (материки, общие подземелья, таверны и прочие) дублируется на клиенте, поэтому когда-нибудь я напишу статью, в которой мы разберем формат файлов и сделаем геометрию для сервера честно. Пока я не настолько хорош.

К счастью, мы знаем внутриигровые координаты персонажа, и стартовый данж — это набор прямоугольных комнат и коридоров, а объектов сложной формы в нем практически нет. Через полчаса хождения по всем углам и записывания координат в блокнотик у нас получится примерно такая картина:

Опорные “точки” геометрии
Опорные “точки” геометрии

Теперь мы можем разметить саму геометрию. Поиск пути на лайв-серверах прекрасен и удивителен, поэтому стопроцентная точность нам не нужна. В Godot 3.4 у меня еще получалось сделать что-то настолько же странное, но после апдейта до 3.5 любые эксперименты, к огромному сожалению, работают лучше, чем лайв.

Геометрия стартового данжа
Геометрия стартового данжа

Монстр расположен в координатах (-1148.9, 4501.6, 1920.5). Создадим для него капсулу с подходящими габаритами (высота 1, радиус 0.3) и отправим его текущие координаты (если изменялись) на клиент, остальное за нас сделает навигация. 

Энкодинг координат для перемещения объектов мы разбирали в прошлой статье, итоговый вид пакета примерно такой:

{
   0x17, 0x00, 0x2c, 0x01, 0x00, x_1, x_2, y_1, z_1, z_2, z_3, 0x2D, id_1, id_2, id_3, 0x6A, 0x10, xdec_1,
   ydec_1, ydec_2, zdec_1, turn_1, turn_2
};

Для поворота нужно преобразовать эйлеров угол [0; 2π) в целое число [0; 256).

Оружие

Монстров можно бить кулаками, но гораздо веселее и продуктивнее делать это чем-то более железным. В стартовом данже на постаменте лежит меч, который игрок может подобрать и взять в руку. 

При подборе предмета клиент может его просто подвинуть или положить в инвентарь. Движение и физику разберем, когда научимся загружать модели объектов на сервере, а пока нас интересует инвентарь.

1a 00 7b 42 8f 02 2c 01 00 f4 03 4f 6f 08 40 c1 10 9e de 00 00 12 16 00 34 00

Похожие пакеты клиент отправляет для большинства действий с предметами. Нас здесь интересуют:

  1. Тип действия (байты 0x80, 0x40,0xC1)

  2. ID предмета (байты 0x12, 0x16, 0x00 со сдвигом на 1 бит влево)

  3. Номер целевого слота (байт 0x34)

  4. Клиентская синхронизация раз (байты 0x9E, 0xDE)

  5. Клиентская синхронизация два (байты 0x03, 0x4F, 0x6F)

В ответ сервер должен отправить:

{
   0x2E, 0x00, 0x2C, 0x01, 0x00, 0x00, 0x00, MinorByte((ushort)clientItemID), MajorByte((ushort)clientItemID),
   0xE8, 0xC7, 0xA0, 0xB0, 0x6E, 0xA6, 0x88, 0x98, 0x95, 0xB1, 0x28, 0x09, 0xDC, 0x85, 0xC8, 0xDF, 0x02, 0x0C,
   MinorByte(clientSyncOther), MajorByte(clientSyncOther), 0x01, 0xFC, clientSync_1, clientSync_2, 0x10, 0x80,
   0x82, 0x20, (byte)(clientSlot_raw * 2), (byte)serverItemID_1, (byte)serverItemID_2, (byte)serverItemID_3,
   0x20, 0x4E, 0x00, 0x00, 0x00
};
  1. ID предмета (clientItemID, без изменений, и serverItemID, со сдвигом на 1 бит влево)

  2. Обе клиентские последовательности синхронизации

  3. Номер целевого слота (сдвинутый на 1 бит влево)

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

19 00 55 42 0b 03 2c 01 00 ae 09 4f 6f 08 40 a3 20 80 a1 41 30 f0 ff ff 0f

Урон

Когда игрок и монстр хорошо проводят время, у них заводится лут. Перед этим важным (и последним) событием в жизни они наносят друг другу урон. Формулу расчета урона оставим на будущее, а пока посмотрим на пакеты.

В пакете от клиента (игрок наносит урон монстру):

20 00 d1 bd a7 02 2c 01 00 60 05 4f 6f 08 40 a3 20 40 e1 97 b0 80 7e 14 ce e0 e9 0d e0 b2 1a 00
  1. Тип действия (0x08, 0x40, 0xA3)

  2. ID клиента (0x05, 0x4F, 0x6F)

  3. ID цели (0xE0, 0xB2, 0x1A)

Сервер в ответ должен прислать:

{
   0x1B, 0x00, 0x2C, 0x01, 0x00, 0x00, 0x04, MinorByte(destId), MajorByte(destId), 0x48,
   0x43, 0xA1, 0x0B, src_1, src_2, src_3, dmg_1, 0xEA, 0x0A, 0x6D, hp_1, hp_2, 0x00,
   0x04, 0x50, 0x07, 0x00
};
  1. ID цели

  2. ID клиента

  3. Урон (точнее, 0x60 - урон * 2)

  4. Здоровье цели после атаки

Для лечения пакет тот же самый, но значение урона должно быть отрицательным (первый бит 1). Если отправить только урон или только текущее здоровье, на клиенте сломается отображение полоски здоровья.

Обычно за убийство монстров игрок получает опыт, но в стартовом данже — немного денег. Вообще говоря, в пакет с наградой можно дописать что угодно, в том числе изменение характеристик персонажа и его координат. В стандартном пакете:

{
   0x04, MinorByte(destId), MajorByte(destId), 0x48, 0x43, 0xA1, 0x09, src_3, src_2, src_1,
   0x00, 0x7E, MinorByte(playerIndexByteSwap), MajorByte(playerIndexByteSwap), 0x08, 0x40,
   0x41, 0x0A, 0x34, 0x3A, 0x93, 0x00, 0x00, 0x7E, 0x14, 0xCE, 0x14, 0x47, 0x81, 0x05, 0x3A,
   0x93, 0x7E, MinorByte(destId), MajorByte(destId), 0x00, 0xC0, src_4, src_5, src_6, 0x01,
   0x58, 0xE4, totalMoney_1, totalMoney_2, 0x16, 0x28, karma_1, 0x80, 0x46, 0x40,
   moneyReward_1, moneyReward_2
};
  1. ID цели

  2. ID источника (для других игроков и монстров пакет такой же)

  3. ID клиента

  4. Количество денег после награды

  5. Текущая карма

  6. Размер награды

Стартовый монстр должен быть именным, но не сегодня
Стартовый монстр должен быть именным, но не сегодня

Цвет текста награды должен быть другим, но системные сообщения (и вообще чат) еще копать и копать.

На этом пока все. В следующий раз будем разбираться с честным добавлением объектов в игровой мир, лутом, торговлей и телепортацией обратно в дивный новый мир. До встречи!

Код проекта на Github

Комментарии (3)


  1. Roland21
    13.11.2022 09:47

    через годик поиграем на задоначенном серве? :)


    1. knelse Автор
      13.11.2022 15:45

      Все может быть :)


  1. nullone
    14.11.2022 02:27

    Когда игрок и монстр хорошо проводят время, у них заводится лут.

    Хороший