Давайте сделаем рогалик (Дистанционный бой)

При первом рассмотрении, кажется, что добавить дистанционные атаки достаточно просто. Мы должны выбрать цель, запустить снаряд и проверить — попали ли мы им куда либо. Рассмотрев задачу более подробно, мы можем разложить ее на несколько простых подзадач, которые нам необходимо реализовать:

  • Система выбора цели
  • Боеприпасы и их количество
  • Система перезарядки оружия
  • Анимация атаки
  • Расчет повреждений

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

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

inv.bi

'Оружие.
 Type weapontype
   id As weaponids           'Тип орежия
   evaldr As Integer         'Сложность опознания. Если > 0 то магический предмет.
   eval As Integer           'Если опознано.
   spell As spellid          'Заклинание.
   noise As Integer          'Кол-во генерируемого шума.
   use As itemuse            'Как используется.
   dam As Integer            'Наносимые повреждения.
   hands As Integer          'Необходимое кол-во рук для использования.
   wslot(1 To 2) As wieldpos 'Занимаемые слоты (до 2-х слотов).
   weapontype As weaptype    'Тип оружия: контактное, дистанционное.
   capacity As Integer       'Емкость обоймы.
   ammotype As ammoids       'Тип используемых снарядов.
   ammocnt As Integer        'Кол-во снарядов в обойме.
 End Type


Перечисление Weaponids позволит легко определить — является оружие контактным или дистанционным.

inv.bi

'Типы оружия.
 Enum weaptype
   wtNone
   wtMelee
   wtProjectile
 End Enum


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

Следующее новое поле в описании типа оружия, это емкость обоймы (в нашем случае - ствол арбалета). Это позволит нам создавать оружие, не требующее перезарядки после каждого выстрела. Большая часть оружия у нас будет однозарядными, так как мы создаем фентезийную игру использующую средневековый стиль оружия. Тем не менее, мы можем использовать эту же структуру и для фантастической игры, где луч лазера может выстрелить раз сто без перезарядки. Удобство данного подхода в том, что данная система также прекрасно работает и для однозарядного оружия, в результате мы получаем большую гибкость кода.

Тип боеприпасов задается перечислением ammoids.

inv.bi

'Типы боеприпасов.
 Enum ammoids
   amNone
   amBagStones
   amCaseBolts
   amQuiverArrows
 End Enum


Здесь у нас задаются три типа боеприпасов: камни для пращи, болты для арбалетов и стрелы для луков. Каждый из них будет содержать количество зарядов. Обратите внимание, что у нас нет боеприпасов по отдельности, т. е. одна стрела или один камень. Я выбрал такой дизайн из за моей нелюбви к боеприпасам по одиночке. Очевидно, что одна стрела, сама по себе, бесполезна, потому я заменю их на «коробки с патронами». Однако это добавит в игру спорный фактор — после выстрела снаряд исчезнет, так как у нас нет снарядов по одиночке. Хотя это может показаться странным, но есть разумное обоснование для этого — боеприпасы становятся бесполезны после их использования. Стрелы и камни, попадая в броню, вероятнее всего разрушились бы в «реальной жизни». Хотя, на самом деле, это всего лишь упрощение и мои собственные предпочтения о вопросе с зарядами.

Последнее поле в описании типа оружия, это ammocnt , которое отвечает за количество снарядов, заряженных в оружие. В большинстве случаев оно будет равно 0 или 1, но в случае с арбалетом, у которого емкость обоймы 6, данный параметр будет варьироваться от 0 до 6.

Когда мы создаем новое оружие нам нужно заполнить эти поля.

inv.bi:GenerateWeapon

Case wpBarrelcrossbow  'двуручное, повреждения 18
         inv.desc = "Barrel Crossbow"
         inv.icon = Chr(209)
         inv.weapon.noise = 12
         inv.weapon.dam = 18
         inv.weapon.hands = 2
         inv.weapon.weapontype = wtProjectile
         inv.weapon.capacity = 6
         inv.weapon.ammotype = amCaseBolts


Это описание для нашего 6-ти зарядного арбалета. Обратите внимание, что емкость обоймы устанавливается равной 6-ти, что позволит нам заряжать в этот арбалет до 6-ти болтов. Тип боеприпасов задан как amCaseBolts, что означает, что данное оружие использует болты а не стрелы или камни.

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

inv.bi

'Боеприпасы.
 Type ammotype
   id As ammoids
   cnt As Integer
   noise As Integer
   eval As Integer
 End Type


Здесь все просто и должно быть понятно. Единственная причина, из за которой мы добавили отдельный тип предмета для зарядов, это возможность хранения персонажем в инвентаре снарядов различных типов для разных типов оружия. Ключевое поле здесь, это col — которое показывает количество боеприпасов содержащихся в данной структуре. Персонажу нет необходимости определять снаряды, так как у них нет каких либо специальных свойств, но мы добавили поле eval, которое необходимо для правильной работы процедуры опознания предметов. Когда мы будем создавать боеприпасы, мы будем отмечать их как уже опознанные, для правильного отображения в инвентаре.

inv.bi

'Создает боеприпасы.
 Sub GenerateAmmo(inv As invtype, currlevel As Integer, ammoid As ammoids = amNone)
   Dim item As ammoids
 
   If ammoid = amNone Then
      item = RandomRange(amBagStones, amQuiverArrows)
   Else
      item = ammoid
   End If
   'Установим идентификатор типа.
   inv.classid = clAmmo
   inv.ammo.id = item
   inv.iconclr = fbCadmiumYellow
   inv.ammo.eval = TRUE
   Select Case item
      Case amBagStones   
         inv.desc = "Bag of Stones"
         inv.ammo.cnt = RandomRange(10, 20)
         inv.ammo.noise = inv.ammo.cnt 
         inv.icon = Chr(167)
      Case amCaseBolts
         inv.desc = "Case of Bolts"
         inv.ammo.cnt = RandomRange(10, 20)
         inv.ammo.noise = inv.ammo.cnt 
         inv.icon = Chr(22)
      Case amQuiverArrows
         inv.desc = "Quiver of Arrows"
         inv.ammo.cnt = RandomRange(10, 20)
         inv.ammo.noise = inv.ammo.cnt 
         inv.icon = Chr(173)
      Case Else
         inv.desc = "Unknown Ammo"
         inv.ammo.cnt = 0
         inv.ammo.noise = 0 
         inv.icon = "?"
   End Select
  
 End Sub


Как видите, тут используется наш стандартный формат процедуры генерации предметов. Все эти подпрограммы идентичны и легко узнаваемы, и, что более важно, это позволяет сразу увидеть что все параметры заданы верно. В результате это поможет избавится от ошибок, так как каждый элемент создается идентичным образом. Используя это идею «шаблонов» в своих программах, вы сделаете код легче читаемым и более простым в отладке и поддержке.

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

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

dod.bas

'Перезарядка дистанционного оружия.
 Sub LoadAmmo ()
   Dim amid As ammoids
   Dim As Integer ret
  
   'Убедимся, что в руках дистанционное оружие.
   If pchar.ProjectileEquipped(wAny) = TRUE Then
      'Проверим первый слот.
      If pchar.ProjectileEquipped(wPrimary) = TRUE Then
         If pchar.IsLoaded(wPrimary) = FALSE Then
            'Получим тип боеприпасов.
            amid = pchar.GetAmmoID(wPrimary)
            'Проверим на правильность типа.
            If amid <> amNone Then
               'Проверим инвентарь на наличие боеприпасов.
               ret = pchar.LoadProjectile(amid, wPrimary)
               If ret = TRUE Then
                  PrintMessage "Weapon is loaded."
               Else
                  PrintMessage "Weapon could not be loaded."
               Endif
            Endif
         Else
            PrintMessage "Weapon is already loaded."
         Endif
      End If
      If pchar.ProjectileEquipped(wSecondary) = TRUE Then
         If pchar.IsLoaded(wSecondary) = FALSE Then
            'Check the inventoru and load weapon if ammo is present.
            ret = pchar.LoadProjectile(amid, wSecondary)
            If ret = TRUE Then
               PrintMessage "Weapon is loaded."
            Else
               PrintMessage "Weapon could not be loaded."
            Endif
         Else
            PrintMessage "Weapon is already loaded."
         Endif
      Endif
   Else
      PrintMessage "Not carrying a projectile weapon."
   Endif
 End Sub


Нам необходимы две проверки — на наличие оружия в основном оружейном слоте, и дополнительном. Большинство дистанционного оружия используют оба оружейных слота, за исключением пращи, которая может быть использована одной рукой. Если бы у нас не было одноручного дистанционного оружия, то нам не понадобилась бы дополнительная проверка вторичного оружейного слота. Так как оба кода идентичны, то рассмотрим проверку только основного слота.

Первое, что нужно сделать, это убедится, что у персонаджа в руках дистанционное оружие, это делает метод ProjectileEquipped объекта персонажа.

character.bi

'Возвращает True если персонаж вооружен дистанционным оружием.
 Function character.ProjectileEquipped(slot As wieldpos) As Integer
   Dim As Integer ret = FALSE
  
   'Убедимся что classid это оружие.
   If _cinfo.cwield(wPrimary).classid = clWeapon Then
      'Убедимся что оружие дистанционное.
      If _cinfo.cwield(wPrimary).weapon.weapontype = wtProjectile Then
         'Проверим слот.
         If (slot = wAny) Or (slot = wPrimary) Then
            ret = TRUE
         End If
      Endif
   Else
      'Убедимся что classid это оружие.
      If _cinfo.cwield(wSecondary).classid = clWeapon Then
         'Убедимся что оружие дистанционное.
         If _cinfo.cwield(wSecondary).weapon.weapontype = wtProjectile Then
            'Проверим слот.
            If (slot = wAny) Or (slot = wSecondary) Then
               ret = TRUE
            End If
         Endif
      Endif
   Endif
    
   Return ret
 End Function


Вначале необходимо убедиться, что персонаж держит в руках оружие (в первичном или вторичном слотах), затем необходимо проверить тип оружия, для того чтобы убедиться что оружие дистанционное. Если оно дистанционное, то мы возвращает true. Обратите внимание, что для указания слооты мы используем не только типы wPrimary и wSecondary, но и тип wAny — указывающий на то, что оружие может быть в любой руке. Тип проверяемого слота передается в функцию и если он будет иметь значение wAny и в первичном слоте обнаружиться дистанционное оружие, то мы выйдем из функции без проверки вторичного слота.

В LoadAmmo после подтверждения нахождения в руках персонажа дистанционного оружия мы переходим к рассмотрению каждого из оружейных слотов по отдельности.

dod.bas:LoadAmmo

...
      If pchar.ProjectileEquipped(wPrimary) = TRUE Then
         If pchar.IsLoaded(wPrimary) = FALSE Then
...


Следующее что нам нужно сделать. это проверить — заряжено ли оружие. Если оно уже заряжено, то перезаряжать его не имеет смысла. Функция IsLoaded сообщает, заряжено ли оружие полностью или нет.

character.bi

'Возвращает true если дистанционное орудие в руках заряжено.
 Function character.IsLoaded(slot As wieldpos) As Integer
   Dim As Integer ret = FALSE
    
   'Убедимся что в руках оружие.
   If _cinfo.cwield(slot).classid = clWeapon Then
      'Убедимся что оно дистанционное.
      If _cinfo.cwield(slot).weapon.weapontype = wtProjectile Then
         'Проверим на кол-во боеприпасов.
         ret = (_cinfo.cwield(slot).weapon.ammocnt < _cinfo.cwield(slot).weapon.capacity)
      Endif
   Endif
    
   Return ret
 End Function


Тут мы просто сравниваем значение параметра оружия ammocnt (заряженных снарядов) и capacity (емкость обоймы). Если у нас оружие многозарядное, то мы можем его перезарядить в любой удобный момент, а не только тогда, когда обойма пуста.

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

dod.bas:LoadAmmo

...
   'Получим идентификатор боеприпасов.
   amid = pchar.GetAmmoID(wPrimary)
   'Проверим на правильность идентификатора.
   If amid <> amNone Then
 ...


Функция GetAmmoId возвращает идентификатор боеприпасов для выбранного оружия.

character.bi

'Возвращает идентификатор боеприпасов для дистанционного оружия в выбранном слоте.
 Function character.GetAmmoID(slot As wieldpos) As ammoids
   Dim ret As armorids
  
   'Убедимся что в слоте оружие.
   If _cinfo.cwield(slot).classid = clWeapon Then
      'Убедимся что оно дистанционное.
      If _cinfo.cwield(slot).weapon.weapontype = wtProjectile Then
         ret = _cinfo.cwield(slot).weapon.ammotype
      Else
         ret = amNone
      Endif
   Else
      ret = amNone
   Endif
   Return ret
 End Function


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

dod.bas:LoadAmmo

...
  'Проверим инвентарь и перезарядим оружие если есть боеприпасы.
  ret = pchar.LoadProjectile(amid, wPrimary)
  If ret = TRUE Then
    PrintMessage "Weapon is loaded."
  Else
    PrintMessage "Weapon could not be loaded."
  Endif
...


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

character.bi

'Заряжает оружие и возвращает True в случае успеха.
 Function character.LoadProjectile(aid As ammoids, slot As wieldpos) As Integer
   Dim As Integer ret = FALSE, i, iitem
   Dim As invtype inv
  
   'Провеним инвентарь на наличие необходимых боеприпасов.
   For i = LowInv To HighInv
     'Выходим, если оружие полностью заряжено.
     If _cinfo.cwield(slot).weapon.ammocnt = _cinfo.cwield(slot).weapon.capacity Then
         Exit For
     Endif
     iitem = HasInvItem(i)
     If iitem = TRUE Then
       'Получим предмет инвентаря.
       GetInventoryItem i, inv
       'Проверим идентификатор предмета.
       If inv.classid = clAmmo Then
          'Проверим тип боеприпасов.
          If inv.ammo.id = aid Then
             'Зарядим оружие до емкости обоймы.
             Do While _cinfo.cwield(slot).weapon.ammocnt < _cinfo.cwield(slot).weapon.capacity
                'Зарядим оружие.
                _cinfo.cwield(slot).weapon.ammocnt += 1
                inv.ammo.cnt -= 1
                'Если закончились боеприпасы — выход из цикла.
                If inv.ammo.cnt = 0 Then
                   Exit Do
                Endif
             Loop
             'Обновим инвентарь.
             If inv.ammo.cnt > 0 Then
                AddInvItem i,inv
             Else
                ClearInv inv
                AddInvItem i,inv
             Endif
             'Если есть заряженные боеприпасы, то вернем true
             If _cinfo.cwield(slot).weapon.ammocnt > 0 Then
                ret = TRUE
             Endif
          Endif
       Endif
     Endif
   Next
  
   Return ret
 End Function


В этой функции мы перебираем все предметы инвентаря, используя свойства LovInv и HighInv, для поиска боеприпасов соответствующих используемому типу оружия. При нахождении мы запускаем цикл, в котором заряжаем оружие по одному заряду. Мы выходим из цикла, как только количество находящихся в обойме снарядов равно емкости обоймы, или закончились боеприпасы в инвентаре. После того как оружие перезаряжено, мы проверяем количество зарядов в объекте инвентаря, и если оно равно 0, то удаляем этот объект, иначе возвращаем его в инвентарь.

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

dod.bas

'Выбор цели и атака дистанционным оружием.
 Sub TargetEnemy ()
   Dim As vec target, source
   Dim As Integer ret
    
   'Проверим, вооружен ли персонаж дистанционным оружием.
   If pchar.ProjectileEquipped(wAny) = TRUE Then
      'Получим вектор цели.
      ret = GetTargetCoord(target)
      source.vx = pchar.Locx
      source.vy = pchar.Locy
      'Игрок нажал enter.
      If ret = TRUE Then
         'Проверим первый слот на дистанционное оружие.
         If pchar.ProjectileEquipped(wPrimary) = TRUE Then
            'Убедимся что заряжено.
            If pchar.IsLoaded(wPrimary) = TRUE Then
               'Выстрелим.
               level.AnimateProjectile source, target
               'Удалим снаряд из оружия.
               pchar.DecrementAmmo wPrimary
               'Убелимся что попали в монстра.
               If level.IsMonster(target.vx, target.vy) = TRUE Then
                  'Нанесем повреждения.
                  DoProjectileCombat target.vx, target.vy, wPrimary
               Else
                  PrintMessage "No monster at target location."
               End If
            Else
               PrintMessage "Weapon is not loaded."
            Endif
         Else 'Возможно в другом слоте?.
            'Проверим, заряжено ли.
            If pchar.IsLoaded(wSecondary) = FALSE Then
               'Выстрелим.
               level.AnimateProjectile source, target
               'Удалим снаряд из оружия.
               pchar.DecrementAmmo wSecondary
               'Убедимся что попали в монстра.
               If level.IsMonster(target.vx, target.vy) = TRUE Then
                  'Нанесем повреждения.
                  DoProjectileCombat target.vx, target.vy, wPrimary
               Else
                  PrintMessage "No monster at target location."
               End If
            Else
               PrintMessage "Weapon is not loaded."
            Endif
         Endif
      Endif
   Else
      PrintMessage "Not carrying a projectile weapon."
   Endif
 End Sub


Вначале мы убеждаемся что в руках персонажа дистанционное оружие и получаем координаты цели при помощи функции GetTargetCoord.

dod.bas

'Возвращает true если персонаж выбрал врага, false — если нет. Координаты сохраняются в векторе vt.
 Function GetTargetCoord(vt As vec) As Integer
   Dim As String ch, rtl = "*"
   Dim As vec v, vtmp
   Dim As Integer ret = FALSE
  
   'Зададим начальные координаты.
   v.vx = pchar.Locx
   v.vy = pchar.Locy
   'Зададим новую цель.
   level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright)
   level.DrawMap
   Do
      ch = Inkey
      If ch <> "" Then
         If ch = key_up Then
            'Зададим текущие координаты.
            vtmp = v
            'Обновим вектор.
            vtmp += north
            'Проверим ячейку карты.
            If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then
               'Очистим предыдущую цель.
               level.SetTarget(v.vx, v.vy, 0)
               'Сохраним текущие координаты.
               v = vtmp
               'Установим новую цель.
               level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright)
               'Перерисуем карту.
               Screenlock
               level.DrawMap
               Screenunlock
            End If
         Endif
         If ch = key_dn Then
            'Зададим текущие координаты.
            vtmp = v
            'Обновим вектор.
            vtmp += south
            'Проверим ячейку карты.
            If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then
               'Очистим предыдущую цель.
               level.SetTarget(v.vx, v.vy, 0)
               'Сохраним текущие координаты.
               v = vtmp
               'Установим новую цель.
               level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright)
               'Перерисуем карту.
               Screenlock
               level.DrawMap
               Screenunlock
            End If
         Endif
         If ch = key_rt Then
            'Зададим текущие координаты.
            vtmp = v
            'Обновим вектор.
            vtmp += east
            'Проверим ячейку карты.
            If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then
               'Очистим предыдущую цель.
               level.SetTarget(v.vx, v.vy, 0)
               'Сохраним текущие координаты.
               v = vtmp
               'Установим новую цель.
               level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright)
               'Перерисуем карту.
               Screenlock
               level.DrawMap
               Screenunlock
            End If
         Endif
         If ch = key_lt Then
            'Зададим текущие координаты.
            vtmp = v
            'Обновим вектор.
            vtmp += west
            'Проверим ячейку карты.
            If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then
               'Очистим предыдущую цель.
               level.SetTarget(v.vx, v.vy, 0)
               'Сохраним текущие координаты.
               v = vtmp
               'Установим новую цель.
               level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright)
               'Перерисуем карту.
               Screenlock
               level.DrawMap
               Screenunlock
            End If
         Endif
      Endif
      Sleep 1
   Loop Until (ch = key_enter) Or (ch = key_esc)
   If ch = key_enter Then
      ret = TRUE
   Endif
   'Очистим предыдущую цель.
   level.SetTarget(v.vx, v.vy, 0)
   level.DrawMap   
   vt = v
  
   Return ret
 End Function


В данной функции мы в цикле опрашиваем нажатые пользователем клавиши и в соответствии с ними перемещаем курсор. Текущие координаты курсора хранятся в переменной векторного типа vt, значение которой вернется в основную программу после завершения выбора цели. Так как игрок не может выбрать цель за пределами видимости персонажа, то мы, вначале, проверяем выбранную ячейку на видимость при помощи функции IsTileVisible. При нажатии клавиши направления мы обновляем позицию курсора путем вызова метода SetTarget объекта уровня, а затем вызываем метод DrawMap для перерисовки карты и отображения курсора на новом месте.

map.bi

'Установить прицел в координаты x, y карты уровня.
 Sub levelobj.SetTarget(x As Integer, y As Integer, id As Integer, tcolor As  Uinteger = fbBlack)
   _level.lmap(x, y).target.id = id
   _level.lmap(x, y).target.tcolor = tcolor
 End Sub


SetTarget обновляет новую структуру данных target, содержащуюся в описании ячейки карты объекта уровня.

map.bi

'Прицел.
 Type targettype
   id As Integer      'Код иконки.
   tcolor As Uinteger 'Цвет иконки.
 End Type
 
  'Информация о ячейке карты
  Type mapinfotype
    terrid As terrainids  'Тип местности.
    monidx As Integer     'Индекс монстра в массиве монстров.
    visible As Integer    'Персонаж видит ячейку.
    seen As Integer       'Персонаж уже видел ячейку.
    doorinfo As doortype  'Информация о двери.
    sndvol As Integer     'Громкость звука в ячейке.
    target As targettype  'Цель и карта снарядов.
  End Type


Структура targettype содержит поля для ASCII символа курсора для указания цели и его цвета. Эта запись находится в структуре mapinfotype, для того, чтобы мы могли его отобразить в каждой ячейке карты уровня.

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

dod.bas:GetTargetCoord

If ch = key_up Then
            'Установим текущий вектор.
            vtmp = v
            'Обновим вектор.
            vtmp += north
            'Проверим ячейку карты.
            If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then
               'Очистим предыдущую цель.
               level.SetTarget(v.vx, v.vy, 0)
               'Сохранить текущую позицию.
               v = vtmp
               'Устновим новую цель.
               level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright)
               'Перерисуем карту.
               Screenlock
               level.DrawMap
               Screenunlock
            End If
         Endif


Здесь обрабатывается случай, когда игрок нажал клавишу вверх. Мы обновляем вектор направления, добавим к текущему значению направление на север, в результате чего он станет указывать на одну ячейку выше текущих координат. Затем мы проверяем — видима ли ячейка карты в полученных координатах. Если это так, то мы очищаем значение курсора передавая 0 в SetTarget, сохраняем полученные координаты в качестве текущих (для использования во время обработки следующей клавиши), обновляем массив цели для отображения курсора в новых координатах, и, наконец, перерисовываем карту уровня. Это обеспечит реанимированное передвижение курсора по видимым ячейкам карты уровня.

dod.bas:GetTargetCoord

Loop Until (ch = key_enter) Or (ch = key_esc)
  If ch = key_enter Then
    ret = TRUE
  Endif


Мы выходим из цикла, когда игрок наживает клавишу Enter или Escape. Если нажат Enter, то мы возвращаем true, что указывает на то, что игрок выбрал цели. При нажатии escape функция вернет false, что говорит нам о том, что выбор цели отменен.

Если функция вернула true, то в TargetEnemy мы обрабатываем выбор цели.

dod.bas:TargetEnemy

'Игрок нажал enter.
      If ret = TRUE Then
         'Проверим первый слот на наличие дистанционного оружия.
         If pchar.ProjectileEquipped(wPrimary) = TRUE Then
            'Убедимся что заряжено.
            If pchar.IsLoaded(wPrimary) = TRUE Then
               'Отобразим анимацию выстрела.
               level.AnimateProjectile source, target


Также, как мы это делали в LoadAmmo, нам нужно проверить что у персонажа есть дистанционное оружие в первичном или вторичном слоте. Проверка одинакова для обоих слотов, поэтому мы рассмотрим проверку только первичного слота. Функция IsLoaded говорит нам о том, что оружие заряжено и готово к выстрелу. Затем мы отображаем анимацию полета снаряда при помощи метода объекта уровня AnimateProjectile.

map.bi

 

'Анимированный полет снаряда.
 Sub levelobj.AnimateProjectile (source As vec, target As vec)
   Dim As Integer x = source.vx, y = source.vy, d = 0, dx = target.vx - source.vx 
   Dim As Integer dy = target.vy - source.vy, c, m, xinc = 1, yinc = 1
   Dim As Integer delay = 10
    
   If dx < 0 Then
      xinc = -1
      dx = -dx
   Endif
   If dy < 0 Then
      yinc = -1
      dy = -dy
   Endif
   If dy < dx Then
      c = 2 * dx
      m = 2 * dy
      Do While x <> target.vx
         _level.lmap(x, y).target.id = 7
         _level.lmap(x, y).target.tcolor = fbYellow
         DrawMap
         Sleep delay
         _level.lmap(x, y).target.id = 0
         _level.lmap(x, y).target.tcolor = fbBlack
         DrawMap
         x += xinc
         d += m
         If d > dx Then
            y += yinc
            d -= c
         Endif
      Loop
   Else
      c = 2 * dy
      m = 2 * dx
      Do While y <> target.vy
         _level.lmap(x, y).target.id = 7
         _level.lmap(x, y).target.tcolor = fbYellow
         DrawMap
         Sleep delay
         _level.lmap(x, y).target.id = 0
         _level.lmap(x, y).target.tcolor = fbBlack
         DrawMap
         y += yinc
         d += m
         If d > dy Then
            x += xinc
            d -= c
         Endif
      Loop
   Endif
   _level.lmap(x, y).target.id = 249
   _level.lmap(x, y).target.tcolor = fbYellow
   DrawMap
   Sleep delay
   _level.lmap(x, y).target.id = 0
   _level.lmap(x, y).target.tcolor = fbBlack
   DrawMap
 End Sub


Начальные координаты полета, это координаты персонажа, а конечные — выбранная игроком цель. Здесь мы используем алгоритм Брезенхэма рисования линий для обновления массива target объекта уровня и перерисовываем карту уровня. При выполнении подпрограммы мы будем видеть анимацию снаряда, летящего к указанной цели.

После анимации выстрела мы возвращаемся в TargetEnemy и уменьшаем количество зарядов в оружии.

dod.bas:TargetEnemy

'Отобразим анимацию выстрела.
   level.AnimateProjectile source, target
   'Удаляем снаряд из оружия.
   pchar.DecrementAmmo wPrimary


Метод объекта персонажа DecrementAmmo удаляет один снаряд из оружия в указанном слоте.

character.bi

'Уменьшим заряды в используемом оружии.
 Sub character.DecrementAmmo(slot As wieldpos)
   _cinfo.cwield(slot).weapon.ammocnt -= 1
   If _cinfo.cwield(slot).weapon.ammocnt < 0 Then
      _cinfo.cwield(slot).weapon.ammocnt = 0
   Endif
 End Sub


Как вы можете видеть, мы просто уменьшаем на 1 значение поля ammocnt у объекта оружия.

Далее необходимо проверить, попали ли мы во что либо и выполнить соответствующие действия.

dod.bas:TargetEnemy

'Удаляем снаряд из оружия.
    pchar.DecrementAmmo wPrimary
    'Проверим есть ли в координатах цели монстр.
    If level.IsMonster(target.vx, target.vy) = TRUE Then
      'Нанесем ему повреждения.
      DoProjectileCombat target.vx, target.vy, wPrimary
    Else
      PrintMessage "No monster at target location."
    End If
  Else
    PrintMessage "Weapon is not loaded."
  Endif


Сначала мы проверяем, есть ли в указанных координатах монстр, и если это так, то нанесем ему повреждения при помощи подпрограммы DoProjectileCombat.

dod.bas

'Применяем повреждения от дистанционной атаки.
 Sub DoProjectileCombat(mx As Integer, my As Integer, wslot As wieldpos)
   Dim As Integer cf, df, croll, mroll, dam, isdead, midx, mxp, xp 
   Dim As String txt, mname
   Dim As Single marm
    
   'Получим фактор защиты монстра.
   df = level.GetMonsterDefense(mx, my)
   mname = level.GetMonsterName(mx, my)
   'Здоровье монстра.
   mxp = level.GetMonsterXP(mx, my)
   'Убедимся что что то есть.
   If df > 0 Then
      'Возвращает боевой фактор в зависимости от того что держит персонаж.
      cf = pchar.GetProjectileCombatFactor()
      'Получим случайные значения.
      croll = RandomRange(1, cf)
      mroll = RandomRange(1, df)
      'Проверим, попал ли персонаж по монстру.
      If croll > mroll Then
         'Получим общий урон наносимый оружием.
         dam = pchar.GetWeaponDamage(wslot)
         'Получим рейтинг брони монстра.
         marm = level.GetMonsterArmor(mx, my)
         'Уменьшим кол-во наносимых повреждений в зависимости от брони.
         dam = dam - (dam * marm)
         If dam <= 0 Then dam = 1
         'Обновим здоровье монстра.
         isdead = level.ApplyDamage(mx, my, dam)
         'Выведем сообщение.
         If isdead = TRUE Then
            'Добавим опыт персонажу.
            xp = pchar.CurrXP
            xp += mxp
            pchar.CurrXP = xp
            xp = pchar.TotXP
            xp += mxp
            pchar.TotXP = xp
            'Напечатаем сообщение.
            txt = pchar.CharName & " killed the " & mname & " with " & dam & " damage points."
            PrintMessage txt
         Else
            txt = pchar.CharName & " hit the " & mname & " for " & dam & " damage points."
            PrintMessage txt
         Endif
      Else
         txt = pchar.CharName & " missed the " & mname & "."
         PrintMessage txt
      Endif
   Endif
 End Sub


Здесь происходит тоже самое, что и при ближнем бое, за исключением того, что используются значения для дистанционных а не контактных атак. GetProjectileCombatFactor возвращает объединенные боевые факторы.

character.bi

'Возвращает боевой фактор для дистанционных атак.
 Function character.GetProjectileCombatFactor() As Integer
   Return CurrPCF + BonPcf
 End Function


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

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

Перевод на русский: Fantik

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