Давайте сделаем рогалик (Запертые двери)

Раз уж мы внедрили механику сотворения заклинаний, пришло время поработать над тем, чтобы заставить функционировать остальные пункты главного меню. Два из них — это Вскрыть замок (Pick Lock) и Выбить дверь (Bash Door). Оба действия относятся к запертым дверям, поэтому нам необходимо реализовать их в игре, чтобы персонажу было что вскрывать и выбивать. Запертые двери мы интегрируем в наш уже существующий код дверей.
Прежде чем перейти непосредственно к коду размещения дверей, давайте взглянем на структуру (Type Def) нашей двери, чтобы освежить в памяти необходимые данные.

map.bi

'Тип двери.
Type doortype
   locked As Integer   'True, если заперта.
   lockdr As Integer   'Сложность взлома замка.
   dstr As Integer     'Прочность двери (для выбивания).
End Type

Флаг locked указывает, заперта дверь или открыта, где значение TRUE означает запертую дверь. Переменная lockdr — это сложность замка, то есть то, насколько трудно его вскрыть. Мы будем использовать это значение при броске кубика на взлом. Переменная dstr — это прочность двери, и это значение пригодится для броска на выбивание. Это наше определение данных двери, а теперь давайте посмотрим, как мы заполняем эти данные.

map.bi

'Добавление дверей в комнату.
Sub levelobj._AddDoorsToRoom(i As Integer)
    Dim As Integer row, col, dd1, dd2, nid, roll
    
   'Добавляем двери.
   For col = _rooms(i).tl.x To _rooms(i).br.x
     dd1 = _rooms(i).tl.y
     dd2 = _rooms(i).br.y
     'Если в стене есть свободное пространство пола.
     If _level.lmap(col, dd1).terrid = tfloor Then
       'Добавляем дверь.
       _level.lmap(col, dd1).terrid = tdoorclosed
       'Проверяем, заперта ли дверь.
       roll = RandomRange(0, maxlevel * 2)
       _level.lmap(col, dd1).doorinfo.locked = (roll < _level.numlevel)
       If _level.lmap(col, dd1).doorinfo.locked = TRUE Then
         _level.lmap(col, dd1).doorinfo.lockdr = _level.numlevel
         _level.lmap(col, dd1).doorinfo.dstr = _level.numlevel * 10
       End If
     Endif
...

Здесь я показываю код только для одной стены комнаты, поскольку все стены обрабатываются одинаково. Давайте разберем, что мы тут делаем, прежде чем углубляться дальше. Как только комната размещена и соединена с остальными, мы перебираем клетки вдоль её стен в поисках тайла пола (tfloor). Если находим такой, значит, мы наткнулись на проем в стене, который ведет в коридор. Затем мы ставим в этот проем закрытую дверь, что обозначается типом ландшафта tdoorclosed.
После размещения двери мы генерируем случайное число (делаем бросок) от 0 до максимального количества уровней, умноженного на два. Это дает нам диапазон значений от 0 до 100, так как на данный момент maxlevel у нас равен 50. Как и в случае с другими объектами в игре, такой диапазон обеспечивает возрастающую вероятность того, что дверь окажется запертой по мере нашего продвижения вглубь подземелья. На 50-м уровне вероятность встретить запертую дверь составит примерно 50%. Получив результат броска, мы сравниваем его с номером текущего уровня. Если выпавшее число меньше номера текущего уровня, дверь запирается. Мы заполняем структуру данных двери, и вуаля — у нас есть запертая дверь.
Теперь, когда у нас появились запертые двери, нам нужно иметь возможность их открывать. Давайте сначала рассмотрим более простой вариант — выбивание дверей.

dod.bas

'Попытка выбить дверь.
Sub BashDoor
   Dim As vec dt
   Dim As Integer ret, proll
   
   'Проверка наличия запертой двери.
   ret = GetLockedDoor(dt)
   'Запертая дверь найдена.
   If ret = TRUE Then
      'Получаем значение броска персонажа.
      proll = pchar.CurrStr + pchar.BonStr
      ret = level.OpenLockedDoor(dt.vx, dt.vy, proll)
      If ret = TRUE Then
         PrintMessage "You bashed open the door." 'Вы выбили дверь.
      Else
         'Вычитаем 1 единицу здоровья (hp).
         PrintMessage "The door remains closed." 'Дверь осталась закрытой.
         pchar.CurrHP = pchar.CurrHP - 1
      Endif
   Else
      PrintMessage "No locked doors found." 'Запертых дверей не найдено.
   Endif
End Sub


Первое, что нам необходимо сделать — убедиться, что запертая дверь находится вплотную к персонажу. Мы делаем это с помощью функции GetLockedDoor, которую рассмотрим чуть позже. Если запертая дверь рядом, мы рассчитываем силу удара персонажа: текущая сила (pchar.CurrStr) плюс любые бонусы к силе (pchar.BonStr). Затем мы передаем эти данные вместе с координатами запертой двери в функцию OpenLockedDoor. Мы создали OpenLockedDoor еще когда реализовывали заклинание открытия дверей, и здесь мы просто повторно используем этот код. Если дверь поддалась — всё отлично. Если же она не открывается, персонаж получает 1 единицу урона. Это симулирует то, что удары по упрямой двери — далеко не лучший способ её открыть.
Давайте взглянем на функцию GetLockedDoor, так как мы будем использовать её и в подпрограмме взлома замков.

dod.bas

'Ищет дверь по каждой из сторон света.
Function GetLockedDoor(dt As vec) As Integer
   Dim As Integer ret = FALSE
   Dim As terrainids tile
   
   'Проверяем дверь рядом с игроком.
   dt.vx = pchar.Locx
   dt.vy = pchar.Locy
   'Смотрим на север.
   dt += north
   'Является ли тайл дверью?
   tile = level.GetTileID(dt.vx, dt.vy)
   If tile = tdoorclosed Then
      'Проверяем, заперта ли она.
      ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE)
   Endif
   'Проверяем, нашли ли мы дверь.
   If ret = FALSE Then
      'Смотрим на следующую клетку.
      dt.vx = pchar.Locx
      dt.vy = pchar.Locy
      'Смотрим на восток.
      dt += east
      'Является ли тайл дверью?
      tile = level.GetTileID(dt.vx, dt.vy)
      If tile = tdoorclosed Then
         'Проверяем, заперта ли она.
         ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE)
      Endif
   Endif
   'Проверяем, нашли ли мы дверь.
   If ret = FALSE Then
      'Смотрим на следующую клетку.
      dt.vx = pchar.Locx
      dt.vy = pchar.Locy
      'Смотрим на юг.
      dt += south
      'Является ли тайл дверью?
      tile = level.GetTileID(dt.vx, dt.vy)
      If tile = tdoorclosed Then
         'Проверяем, заперта ли она.
         ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE)
      Endif
   Endif
   'Проверяем, нашли ли мы дверь.
   If ret = FALSE Then
      'Смотрим на следующую клетку.
      dt.vx = pchar.Locx
      dt.vy = pchar.Locy
      'Смотрим на запад.
      dt += west
      'Является ли тайл дверью?
      tile = level.GetTileID(dt.vx, dt.vy)
      If tile = tdoorclosed Then
         'Проверяем, заперта ли она.
         ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE)
      Endif
   Endif
   
   Return ret
End Function

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



dod.bas:GetLockedDoor
   'Проверяем дверь рядом с игроком.
   dt.vx = pchar.Locx
   dt.vy = pchar.Locy
   'Смотрим на север.
   dt += north
   'Является ли тайл дверью?
   tile = level.GetTileID(dt.vx, dt.vy)
   If tile = tdoorclosed Then
      'Проверяем, заперта ли она.
      ret = (level.IsDoorLocked(dt.vx, dt.vy) = TRUE)
   EndIf


Сначала мы берем текущую позицию персонажа и записываем её в объект вектора. Затем вызываем перегруженный оператор += объекта вектора, чтобы получить новые координаты для проверки. Мы перехватываем ID тайла в этой точке и проверяем, является ли он закрытой дверью (tdoorclosed). Если да, мы проверяем, заперта ли она, с помощью функции IsDoorLocked.

map.bi

'Возвращает True, если дверь заперта.
Function levelobj.IsDoorLocked(x As Integer,y As Integer) As Integer
   Return _level.lmap(x, y).doorinfo.locked
End Function

Как видите, мы просто возвращаем состояние флага locked для переданных координат. Этот флаг возвращается в функцию GetLockedDoor, которая, в свою очередь, передает его обратно в подпрограмму выбивания дверей (BashDoor).
Выбивание — лишь один из способов открыть дверь. Персонаж также может вскрыть замок, так что давайте рассмотрим этот метод следующим.
Прежде чем мы перейдем к коду взлома, нам нужно добавить в игру инструменты для вскрытия замков. Я сделал отмычки расходуемым предметом снабжения (supply item), чтобы мы могли задействовать уже существующую кодовую базу. Вот новые предметы для взлома замков:



inv.bi
'Идентификаторы предметов снабжения.
Enum supplyids
   supSupplyNone  'Ничего.
   supHealingHerb 'Целебная трава — исцеляет 50% от макс. HP.
   supHunkMeat    'Кусок мяса — исцеляет 25% от макс. HP.
   supBread       'Хлеб — исцеляет 10% от макс. HP.
   supManaOrb     'Сфера маны — восстанавливает 10% маны.
   supLockPick    'Набор отмычек.
   supSkeletonKey 'Отмычка-открывашка (мастер-ключ).
End Enum


Для некоторого разнообразия мы добавили в игру отмычки (lock picks) и мастер-ключи (skeleton keys). Оба предмета могут отпереть замок, если персонаж экипировал их, находясь рядом с запертой дверью. Несмотря на то, что эти предметы относятся к категории снабжения (supply items), персонаж должен держать их в руках как оружие. Поэтому нам нужно обновить структуру типа снабжения, добавив позиции для экипировки.

inv.bi

'Определение структуры типа предметов снабжения.
Type supplytype
   id As supplyids      'Указывает, какой именно предмет находится в структуре.
   desc As String * 30  'Описание эффекта предмета.
   evaldr As Integer    'Рейтинг сложности распознавания. Используется для оценки магических эффектов: 0 = неМагический.
   eval As Integer      'True, если предмет был успешно распознан.
   spell As spelltype   'Тип магического эффекта.
   noise As Integer     'Количество шума, создаваемого предметом (при использовании и в инвентаре).
   use As itemuse       'Способ использования предмета.
   sslot(1 To 2) As wieldpos 'Слоты экипировки для отмычек и мастер-ключей.
End Type


Обратите внимание, что мы добавили поле sslot — оно ведет себя точно так же, как слот для оружия. Это отчетливо видно в обновленной процедуре генерации предметов снабжения.

inv.bi

'Генерация нового предмета снабжения.
Sub GenerateSupplies(inv As invtype, currlevel As Integer)

...

      Case supLockPick    'Набор отмычек.
         inv.desc = "Lock Pick"
         inv.supply.desc = "Opens Lock"
         inv.supply.noise = 3
         inv.icon = Chr(168)
         inv.iconclr = fbSilver
         inv.supply.eval = FALSE
         inv.supply.use = useWieldWear
         inv.supply.sslot(1) = wPrimary
         inv.supply.sslot(2) = wSecondary
      Case supSkeletonKey 'Мастер-ключ.
         inv.desc = "Skeleton Key"
         inv.supply.desc = "Opens Lock"
         inv.supply.noise = 1
         inv.icon = Chr(13)
         inv.iconclr = fbSilver
         inv.supply.eval = FALSE
         inv.supply.use = useWieldWear
         inv.supply.sslot(1) = wPrimary
         inv.supply.sslot(2) = wSecondary
      Case Else
         inv.desc = "Uknown Supply"
         inv.supply.noise = 0
         inv.icon = Chr(63)
         inv.iconclr = fbYellow
         inv.supply.eval = FALSE
         inv.supply.use = useNone
   End Select
End Sub

Перед вами два новых предмета снабжения и данные для их инициализации. Заметьте, что мы устанавливаем позиции экипировки в wPrimary (основная рука) и wSecondary (левая рука/щит), а флаг использования — в useWieldWear. Благодаря этим настройкам персонаж сможет брать данные инструменты в руки точно так же, как обычное оружие, и наш новый код экипировки отработает для них корректно и предсказуемо.
Как я уже говорил в прошлой главе, мы стремимся избегать исключений в логике везде, где это возможно, поскольку исключения по определению усложняют код. Эти простые дополнения к структуре снабжения гарантируют, что отмычка и мастер-ключ органично встроятся в готовую систему. Разумеется, остальным процедурам работы с инвентарем тоже потребуются минимальные обновления для корректной обработки этих предметов.
Теперь мы готовы разобрать код взлома замков.



dod.bas
'Попытка взломать замок отмычкой.
Sub PickLock
   Dim As vec dt
   Dim As Integer ret = FALSE, proll, haslp = FALSE
   Dim As invtype inv
   
   'Ищем запертую дверь поблизости, если она есть.
   ret = GetLockedDoor(dt)
   'Если дверь найдена, проверяем, взят ли в руки мастер-ключ или отмычка.
   If ret = TRUE Then
      'Проверяем основной слот оружия (правая рука).
      pchar.GetInventoryItem wPrimary, inv
      'Отмычки и мастер-ключи относятся к классу предметов снабжения.
      If inv.classid = clSupplies Then
         If (inv.supply.id = supLockPick) Or (inv.supply.id = supSkeletonKey) Then
            'Инструмент взлома обнаружен в руке.
            haslp = TRUE
         End If
      End If
      'Если в основном слоте ничего нет, проверяем дополнительный слот.
      If haslp = FALSE Then
         pchar.GetInventoryItem wSecondary, inv
         If inv.classid = clSupplies Then
            If (inv.supply.id = supLockPick) Or (inv.supply.id = supSkeletonKey) Then
               'Инструмент взлома обнаружен в левой руке.
               haslp = TRUE
            End If
         Endif
      Endif
      
      'Если отмычка или мастер-ключ в руках, пытаемся открыть дверь.
      If haslp = TRUE Then
         'Проверяем, опознан (идентифицирован) ли предмет.
         If IsEval(inv) = TRUE Then
            'Если предмет опознан, его сложность распознавания (dr) добавляется как бонус к броску игрока.
            proll = pchar.CurrDex + pchar.BonDex + inv.supply.evaldr 
         Else
            'Предмет не опознан.
            proll = pchar.CurrDex + pchar.BonDex
         Endif
         'Пытаемся открыть запертую дверь.
         ret = level.OpenLockedDoor(dt.vx, dt.vy, proll)
         If ret = TRUE Then
            PrintMessage "You opened the door." 'Вы открыли дверь.
         Else
            PrintMessage "You failed to open the door." 'Вам не удалось открыть дверь.
         Endif
      Else
         PrintMessage "You must equip a lock pick or skeleton key to open a locked door." 'Вы должны экипировать отмычку или мастер-ключ, чтобы открыть запертую дверь.
      Endif
   Else
      PrintMessage "No locked doors found." 'Запертых дверей не найдено.
   Endif
End Sub


Этот код немного сложнее, чем алгоритм выбивания двери, но большая его часть — это просто проверка наличия в руках отмычки или мастер-ключа. Первым делом мы ищем запертую дверь с помощью написанной ранее функции GetLockedDoor. Если дверь рядом, мы проверяем, держит ли персонаж один из инструментов взлома. Нам приходится сканировать как основной (wPrimary), так и дополнительный (wSecondary) слоты, ведь инструмент может находиться в любом из них. Если персонаж не держит нужный предмет, переменная haslp останется равной FALSE, и игра сообщит пользователю, что инструмент необходимо сначала экипировать. Если же инструмент в руках, мы проверяем, был ли он распознан (идентифицирован).
Это очень важный шаг. В качестве базы для броска персонажа мы берем его характеристику Ловкости (pchar.CurrDex + pchar.BonDex). Если инструмент был успешно распознан игроком ранее, персонаж получает к броску приятный бонус, равный рейтингу сложности распознавания этого предмета (inv.supply.evaldr). Если же инструмент не опознан, бонус не начисляется. Это придает механике идентификации реальный практический смысл в контексте данных инструментов. Не будь этого бонуса, процесс распознавания стал бы бесполезной рутиной, которой игроки бы просто пренебрегали. Любые действия в игре должны иметь последствия — хорошие или плохие, — поэтому мы обязаны вознаграждать игрока за усилия, потраченные на опознание вещей. Рассчитав финальное значение броска, мы передаем его в функцию OpenLockedDoor для вычисления результата.
Поскольку взлом дверей — это действие, требующее мастерства персонажа, нам также следует поощрять его за успех. Мы обновили функцию OpenLockedDoor, чтобы она начисляла игроку очки опыта при успешном открытии замка.

map.bi

'Попытка открыть запертую дверь.
Function levelobj.OpenLockedDoor(x As Integer, y As Integer, dr As Integer) As Integer
   Dim As Integer ret = TRUE, ddr, rolld, rollp
   Dim tid As terrainids
   
   'Убеждаемся, что перед нами действительно дверь.
   tid = GetTileID(x, y)
   If tid = tDoorClosed Then
     If IsDoorLocked(x, y) = TRUE Then
        'Получаем рейтинг сложности замка на двери.
        ddr = _level.lmap(x, y).doorinfo.lockdr
        'Делаем броски кубиков.
        rollp = RandomRange(1, dr)  'Бросок игрока
        rolld = RandomRange(1, ddr) 'Бросок двери
        If rollp > rolld Then
           'Открываем дверь.
           _level.lmap(x, y).doorinfo.locked = FALSE
           SetTile x, y, tdooropen
           'Начисляем персонажу очки опыта.
           pchar.CurrXP = pchar.CurrXP + ddr 
        Else
           'Дверь не поддалась.
           ret = FALSE
        Endif
     Endif
   Endif
   
   Return ret
End Function

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

Перевод на русский: GOOGLE AI ASSISTANT

содержание | назад | вперед