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

spells.png

Теперь,когда у нас есть изученные заклинания, перейдем к их использованию персонажем. Чтобы вызвать какое либо заклинание, игрок использует клавишу «c» которая обрабатывается следующим кодом в основном цикле игры:

dod.bas

'Вызов заклинания.
  If ckey = "c" Then
    CastSpell
    level.MoveMonsters
    DrawMainScreen
  Endif

Как вы можете видеть, мы вызываем новую подпрограмму CastSpell, которую добавили в файл dod.bas

dod.bas
'Вызов заклинания.
  Sub CastSpell ()
    Dim As Integer splcnt, i, iitem, ret, mc, md, rollm, rollp
    Dim As Integer tmp, snd, cancel = FALSE
    Dim As invtype sinv, iinv
    Dim As tWidgets.listtype splist()
    Dim As tWidgets.btnID btn
    Dim As tWidgets.tList lst
    Dim As vec vt, pt
    Dim tid As terrainids
     
    'Проверим список заклинаний.
    For i = pchar.LowISpell To pchar.HighISpell
      iitem = pchar.HasInvItem(i)
      If iitem = TRUE Then
         'Получим предмет инвентаря.
         pchar.GetInventoryItem i, sinv
         'Добавим название заклинания в список.
         splcnt += 1
         Redim Preserve splist(1 To splcnt)
         'Используем индекс в качестве id.
         splist(splcnt).id = i
         splist(splcnt).text = sinv.spell.splname
      Endif
   Next
   'Убедимся, что есть заклинание.
   If splcnt > 0 Then
      'Установим id равное 0.
      iitem = 0
      'Настроим окно со списком.
      lst.Title = "Select Spell to Cast"
      lst.Prompt = "Use Up or Dn key to cycle spells, Enter to select."
      'Получим выбор игрока.
      btn = lst.Listbox(splist(), iitem)
      'Проверим, что игрок не нажал отмену.
      If (btn <> tWidgets.gbnCancel) And (iitem <> 0) Then
         'Получим предмет инвентаря, используя полученный id.
         pchar.GetInventoryItem iitem, sinv
         'Убедимся что у персонажа достаточно маны.
         If sinv.spell.manacost > pchar.CurrMana Then
            ShowMsg "Mana", "You do not have enough Mana to cast spell.", tWidgets.MsgBoxType.gmbOK
         Else
            'Проверим цель заклинания.
            ret = splSet.IsMember(sinv.spell.id)
            'This is a target spell.
            If ret = TRUE Then
               'Получим координаты цели.
               ret = GetTargetCoord(vt)
               'Убедимся, что игрок указал цель.
               If ret = TRUE Then
                  'Уьедимся в правильности цели.
                  If level.IsMonster(vt.vx, vt.vy) = FALSE Then
                     ShowMsg "Target", "Nothing to target!", tWidgets.MsgBoxType.gmbOK
                  Else
                     'Нарисуем магическую атаку.
                     pt.vx = pchar.Locx
                     pt.vy = pchar.Locy
                     level.AnimateProjectile pt, vt
                     'Получим фактор магической защиты монстра.
                     md = level.GetMonsterMagicDefense(vt.vx, vt.vy)
                     'Получим фактор магической атаки персонажа.
                     mc = pchar.CurrMcf + pchar.BonMcf
                     'Возьмем случайные значения.
                     rollp = RandomRange(1, mc) 'атака
                     rollm = RandomRange(1, md) 'защита
                     'Персонаж поразил цель?
                     If rollp > rollm Then
                        'Применим эффект заклинания на монстра.
                        ret = level.ApplySpell(sinv.spell, vt.vx, vt.vy)
                     Else
                        'Сообщение о промахе.
                        PrintMessage "The " & level.GetMonsterName(vt.vx, vt.vy) & " dispels the " & sinv.spell.splname & "!"
                     Endif
                  Endif
               End If
            Else
               Select Case sinv.spell.id
                  Case splHeal
                     tmp = pchar.MaxHP * (sinv.spell.lvl / 100)
                     If tmp < 1 Then tmp = 1
                     pchar.CurrHP = pchar.CurrHP + tmp
                  Case splMana
                     tmp = pchar.MaxMana * (sinv.spell.lvl / 100)
                     If tmp < 1 Then tmp = 1
                     pchar.CurrMana = pchar.CurrMana + tmp
                  Case splRecharge
                     'Найдем палочки в инвертаре и зарядим в соответствии с уровнем.
                     For i = pchar.LowInv To pchar.HighInv
                        iitem = pchar.HasInvItem(i)
                        If iitem = TRUE Then
                           'Получим предметы инвентаря.
                           pchar.GetInventoryItem i, iinv
                           'Проверим, оружие ли это.
                           If iinv.classid = clWeapon Then
                              'Проверим — палочка ли это.
                              If iinv.weapon.iswand = TRUE Then
                                 'Увеличим заряды.
                                 iinv.weapon.ammocnt += sinv.spell.lvl
                                 'Убедимся, что не превысили максимальное кол-во зарядов.
                                 If iinv.weapon.ammocnt > iinv.weapon.capacity Then
                                    iinv.weapon.ammocnt = iinv.weapon.capacity
                                 Endif
                                 'Вернем предмет в инвентарь.
                                 pchar.AddInvItem iitem, iinv
                              Endif
                           Endif
                        End If
                     Next
                     'Проверим в слотах экипировки и тоже зарядим, если возможно
                     iitem = pchar.HasInvItem(wPrimary)
                     If iitem = TRUE Then
                        pchar.GetInventoryItem wPrimary, iinv
                        If iinv.classid = clWeapon Then
                           'Проверим — палочка ли это.
                           If iinv.weapon.iswand = TRUE Then
                              iinv.weapon.ammocnt += sinv.spell.lvl
                              If iinv.weapon.ammocnt > iinv.weapon.capacity Then
                                 iinv.weapon.ammocnt = iinv.weapon.capacity
                              Endif
                              pchar.AddInvItem wPrimary, iinv
                           Endif
                        Endif
                     Endif
                     'Проверим следующий слот.
                     iitem = pchar.HasInvItem(wSecondary)
                     If iitem = TRUE Then
                        pchar.GetInventoryItem wSecondary, iinv
                        If iinv.classid = clWeapon Then
                           'Проверим — палочка ли это.
                           If iinv.weapon.iswand = TRUE Then
                              iinv.weapon.ammocnt += sinv.spell.lvl
                              If iinv.weapon.ammocnt > iinv.weapon.capacity Then
                                 iinv.weapon.ammocnt = iinv.weapon.capacity
                              Endif
                              pchar.AddInvItem wSecondary, iinv
                           Endif
                        Endif
                     Endif
                  Case splFocus
                     'Увеличим все боевые факторы на 1 ход.
                     pchar.BonUcf = sinv.spell.lvl
                     pchar.BonUcfCnt = 1
                     pchar.BonAcf = sinv.spell.lvl
                     pchar.BonAcfCnt = 1
                     pchar.BonPcf = sinv.spell.lvl
                     pchar.BonPcfCnt = 1
                     pchar.BonCdf = sinv.spell.lvl
                     pchar.BonCdfCnt = 1
                     pchar.BonMcf = sinv.spell.lvl
                     pchar.BonMcfCnt = 1
                     pchar.BonMdf = sinv.spell.lvl
                     pchar.BonMdfCnt = 1
                  Case splTeleport
                     'Получим координаты цели.
                     ret = GetTargetCoord(vt, sinv.spell.lvl)
                     If ret = TRUE Then
                        'Проверим, есть ли монстр.
                        If level.IsMonster(vt.vx, vt.vy) = TRUE Then
                           'Телепорт в монстра убивает его.
                           ret = level.ApplySpell(sinv.spell, vt.vx, vt.vy)
                           'Установим новые координаты персонажа.
                           pchar.Locx = vt.vx
                           pchar.Locy = vt.vy
                           'Сгенерируем карту звуков.
                           level.ClearSoundMap
                           snd = pchar.GetNoise()
                           level.GenSoundMap(pchar.Locx, pchar.Locy, snd)
                        Else
                           'Убедимся, что тайл карты не блокирован.
                           If level.IsBlocking(vt.vx, vt.vy) = FALSE Then
                              'Установим новые координаты персонажа.
                              pchar.Locx = vt.vx
                              pchar.Locy = vt.vy
                              'Сгенерируем карту звуков.
                              level.ClearSoundMap
                              snd = pchar.GetNoise()
                              level.GenSoundMap(pchar.Locx, pchar.Locy, snd)
                           Else
                              ShowMsg "Teleport", "You can't teleport there.", tWidgets.MsgBoxType.gmbOK
                              cancel = TRUE
                           End If  
                        Endif
                     Else
                        cancel = TRUE
                     Endif
                  Case splOpen
                     'Получим координаты цели.
                     ret = GetTargetCoord(vt, sinv.spell.lvl)
                     If ret = TRUE Then
                        'Получим id местности.
                        tid = level.GetTileID(vt.vx, vt.vy)
                        'Проверим на наличие закрытой двери.
                        If tid = tDoorClosed Then
                           'Проверим, заперта ли она.
                           If level.IsDoorLocked(vt.vx, vt.vy) = TRUE Then
                              ret = level.OpenLockedDoor(vt.vx, vt.vy, sinv.spell.lvl)
                              If ret = TRUE Then
                                 PrintMessage "Door was opened."
                              Endif
                           Else
                              'Сообщим игроку, что дверь не щаперта.
                              ShowMsg "Open Spell", "The door is not locked.", tWidgets.MsgBoxType.gmbOK
                              cancel = TRUE
                           Endif
                        Endif
                     Else
                        cancel = TRUE
                     End If
                  Case splBlink
                     'Установим эффект ослепления.
                     pchar.SetSpellEffect sinv.spell.id, sinv.spell.lvl, 0
               End Select
            Endif
            If cancel = FALSE Then
               'Уменьшим ману на стоимость заклинания.
               pchar.CurrMana = pchar.CurrMana - sinv.spell.manacost
            End If
         Endif
      Endif
    Else
      ShowMsg "Spells", "You have not learned any spells.", tWidgets.MsgBoxType.gmbOK
    Endif
     
  End Sub

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

dod.bas:CastSpell
...
   'Проверим список заклинаний.
    For i = pchar.LowISpell To pchar.HighISpell
      iitem = pchar.HasInvItem(i)
      If iitem = TRUE Then
         'Получим предмет инвентаря.
         pchar.GetInventoryItem i, sinv
         'Добавим название заклинания в список.
         splcnt += 1
         Redim Preserve splist(1 To splcnt)
         'Используем индекс в качестве идентификатора.
         splist(splcnt).id = i
         splist(splcnt).text = sinv.spell.splname
      Endif
    Next
  ...

В этом For-Next цикле, мы проверяем все слоты инвентаря заклинаний на наличие в них заклинаний. Мы используем два новых свойства объекта персонажа, pchar.LowISpell и pchar.HighISpell, которые содержат значения минимального и максимального индекса массива заклинаний.

character.bi
'Возвращает минимальный индекс массива заклинаний.
  Property character.LowISpell() As Integer
    Return Lbound(_cinfo.cspells)
  End Property
 
  'Возвращает максимальный индекс массива заклинаний.
  Property character.HighISpell() As Integer
    Return Ubound(_cinfo.cspells)
  End Property

В цикле, при помощи функции HasInvItem мы проверяем, содержится ли в данном слоте заклинание, и если это так, то получаем его при помощи функции GetInventoryItem. В следующей части кода мы копируем информацию о полученном объекте в SPList, тип которого определен в tWidgets объекте. Этот список будет передан объекту tWidgets для отображения списка заклинаний, чтобы дать возможность игроку выбрать заклинание из этого списка.

dod.bas:CastSpell
...
    'Убедимся, что есть заклинание.
    If splcnt > 0 Then
      'Установим id равное 0.
      iitem = 0
      'Настроим окно со списком.
      lst.Title = "Select Spell to Cast"
      lst.Prompt = "Use Up or Dn key to cycle spells, Enter to select."
      'Получим выбор игрока.
      btn = lst.Listbox(splist(), iitem)
      'Убедимся, что игрок не нажал на отмену.
      If (btn <> tWidgets.gbnCancel) And (iitem <> 0) Then
         'Получим предмет инвентаря, в соответствии с полученным идентификатором.
         pchar.GetInventoryItem iitem, sinv
         'Убедимся, что у персонажа достаточно маны.
         If sinv.spell.manacost > pchar.CurrMana Then
            ShowMsg "Mana", "You do not have enough Mana to cast spell.", tWidgets.MsgBoxType.gmbOK
         Else
  ...

Сначала мы должны проверить, что у нас есть заклинания для отображения. Для этого мы используем переменную splcnt. Если заклинания найдены, то мы отображаем список заклинаний, содержащийся в splist(). Игрок будет иметь возможность выбрать заклинание из списка, или нажать Escape для отмены (Объект List будет рассмотрен в приложении, наряду с другими объектами tWidget).

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

dod.bas:CastSpell
...
    'Проверим цель заклинания.
    ret = splSet.IsMember(sinv.spell.id)
  ...

Существует два типа заклинаний, одни наносят ущерб противнику, другие, наоборот, улучшают состояние заклинателя, например исцеление или улучшение боевых характеристик. Обратите внимание, что мы создали новый объект для хранения идентификаторов атакующих заклинаний, и, при помощи функции IsMember, мы проверяем, содержится ли выбранное заклинание в этом списке. Новый объект находится в файле set.bi.

set.bi
'Проверим на существующие определения.
  #IFNDEF NULL
  #DEFINE NULL 0
  #ENDIF
  #IFNDEF FALSE
  #DEFINE FALSE 0
  #DEFINE TRUE (Not FALSE)
  #ENDIF
   
  Type setobj
    Private:
    _set As Integer Ptr 'Набор элементов.
    _setcnt As Integer  'Кол-во элементов в наборе.
    Declare Sub _DestroySet() 'Очистить объект и освободить память.
    Public:
    Declare Constructor ()
    Declare Destructor ()
    Declare Function AddToSet (item As Integer)As Integer 'Вернет TRUE если объект добавлен.
    Declare Function IsMember(item As Integer) As Integer 'Вернет TRUE если объект в наборе.
  End Type
 
  'Очищает объекты и освобождает память.
  Sub setobj._DestroySet()
    If _set <> NULL Then
      Deallocate _set
      _set = NULL
    Endif
  End Sub
  'Конструктор ничего не делает в данный момент.
  Constructor setobj ()
    _DestroySet
  End Constructor
   
  'Деструктор, очищает объекты.
  Destructor setobj ()
    _DestroySet
  End Destructor
   
  'Возвращает TRUE если предмет добавлен.
  Function setobj.AddToSet (item As Integer)As Integer
    Dim As Integer ret = TRUE
     
    If _set = NULL Then
      _setcnt += 1
      _set = Callocate(_setcnt, Sizeof(Integer))
      _set[_setcnt - 1] = item
    Else
      'Проверим на присутствие предмета.
      For i As Integer = 0 To _setcnt - 1
         If _set[i] = item Then
            ret = FALSE
            Exit For
         Endif
      Next
      'Если не нашли.
      If ret = TRUE Then
         _setcnt += 1
         _set = Reallocate(_set, _setcnt * Sizeof(Integer))
         If _set <> NULL Then
            _set[_setcnt - 1] = item
         Endif
      Endif
    Endif
     
    Return ret
  End Function
   
  'Возвращает TRUE если предмет в наборе.
  Function setobj.IsMember(item As Integer) As Integer
    Dim As Integer ret = FALSE
     
    If _set <> NULL Then
      For i As Integer = 0 To _setcnt - 1
         If _set[i] = item Then
            ret = TRUE
            Exit For
         Endif
      Next
    Endif
     
    Return ret
  End Function

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

Метод AddToSet добавляет объект в набор. Мы используем массив указателей, так как не можем использовать динамические массивы в определении типа и должны сами управлять используемой памятью. AddToSet перераспределяет память расширяя массив указателей и добавляет в него новый элемент. Но прежде чем это сделать, мы должны убедиться, что в наборе уже не присутствует данные элемент, так как все элементы должны быть уникальными и добавление дублирующихся объектов в любом случае не имеет смысла. Основная цель сохранения объектов в наборе, это определение того, что какой либо предмет принадлежит определенной коллекции, как, например, мы делаем это в подпрограмме CastSpell. Разумеется, изначально мы должны заполнить набор элементами, что мы делаем в процедуре InitTargetSpells.

inv.bi
'Инициализируем коллекцию заклинаний, для которых необходимо указание цели.
  Sub InitTargetSpells()
    Dim As Integer ret
    
    'Добавим заклинания в коллекцию.
    ret = splSet.AddToSet(splAcidFog)
    ret = splSet.AddToSet(splFireCloak)
    ret = splSet.AddToSet(splLightning)
    ret = splSet.AddToSet(splBlind)
    ret = splSet.AddToSet(splFear)
    ret = splSet.AddToSet(splConfuse)
    ret = splSet.AddToSet(splFireBomb)
    ret = splSet.AddToSet(splEntangle)
    ret = splSet.AddToSet(splCloudMind)
    ret = splSet.AddToSet(splFireball)
    ret = splSet.AddToSet(splIceStatue)
    ret = splSet.AddToSet(splRust)
    ret = splSet.AddToSet(splShatter)
    ret = splSet.AddToSet(splMagicDrain)
    ret = splSet.AddToSet(splEnfeeble)
    ret = splSet.AddToSet(splStealHealth)
    ret = splSet.AddToSet(splMindBlast)
  End Sub

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

dod.bas
'Используем разрешение 640x480 32bit с текстом 80x60 символов.
  Screenres sw, sh, 32
  Width txcols, txrows
  Windowtitle "Dungeon of Doom"
  Randomize Timer 'Установим генератор случайных чисел.
  tWidgets.InitWidgets 'Инициализируем виджеты.
   
  'Инициализируем список заклинаний для которых необходимо указание цели.
  InitTargetSpells
  'Нарисуем титульный экран.
  DisplayTitle

Сам объект задан в файле defs.bi как общая (Shared) переменная.

defs.bi
'Список сообщений.
  Dim Shared mess(1 To 4) As String
  Dim Shared messcolor(1 To 4) As Uinteger = {fbWhite, fbWhite1, fbWhite2, fbWhite3}
  'Набор заклинаний.
  Dim Shared splSet As setobj

Это может показаться излишним — создавать отдельный объект для набора значений, хотя мы могли просто создать массив с этими значениями. Но что если в определенный момент нам понадобиться другой набор, или много разных наборов. Мы могли бы использовать полдюжины различных массивов для этих целей, но выглядело бы это «грязно». С объектом, же, независимо от того, сколько наборов нам потребуется, мы просто проинициализируем объект новыми значениями. Даже если нам не понадобятся больше коллекции элементов, обхем кода для описания этого объекта не превышает объем кода, который бы нам пришлось написать для работы с предопределенным массивом. Поэтому, в любом случае, лучше использовать данный объект.

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

dod.bas:CastSpell
...
    'Получим координаты цели.
    ret = GetTargetCoord(vt)
    'Убедимся, что игрок выбрал цель.
    If ret = TRUE Then
     'Убедимся что цель верная.
     If level.IsMonster(vt.vx, vt.vy) = FALSE Then
       ShowMsg "Target", "Nothing to target!", tWidgets.MsgBoxType.gmbOK
     Else
       'Отобразим анимацию атаки.
       pt.vx = pchar.Locx
       pt.vy = pchar.Locy
       level.AnimateProjectile pt, vt
  ...

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

dod.bas:CastSpell
...
       'Получим фактор магической защиты монстра.
       md = level.GetMonsterMagicDefense(vt.vx, vt.vy)
       'Получим фактор магической атаки персонажа.
       mc = pchar.CurrMcf + pchar.BonMcf
       'Получим случайные значения.
       rollp = RandomRange(1, mc) 'offense
       rollm = RandomRange(1, md) 'defense
       'Персонаж попал по цели?
       If rollp > rollm Then
         'Назначим эффект заклинания монстру.
         ret = level.ApplySpell(sinv.spell, vt.vx, vt.vy)
       Else
         'Сообщение о промахе.
         PrintMessage "The " & level.GetMonsterName(vt.vx, vt.vy) & " dispels the " & sinv.spell.splname & "!"
       Endif
     Endif
    End If
  ...

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

map.bi
Case splStealHealth
            ret = ApplyDamage(mx, my, dam)
            pchar.CurrHP = pchar.CurrHP + dam
            txt = "Steal Health Spell stole  " & dam & " health from " & _level.moninfo(midx).mname & "."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splLightning
            ret = ApplyDamage(mx, my, spl.dam)
            txt = "Lightning Spell inflicted  " & spl.dam & " damage to " & _level.moninfo(midx).mname & "."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splAcidFog     'КнигаЗакл.: 5 повреждений каждые lvl ходов
            ret = ApplyDamage(mx, my, dam)
            'Если не умер, устанавливаем счетчик времени.
            If ret = FALSE Then
               _level.moninfo(midx).effects(meAcidFog).cnt = spl.lvl
               _level.moninfo(midx).effects(meAcidFog).dam = dam
            End If
            txt = "Acid Fog Spell inflicted  " & dam & " damage to " & _level.moninfo(midx).mname & " for " & spl.lvl & " turns."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splFireCloak   'КнигаЗакл.: Цель получает 10 повреждений lvl ходов
            ret = ApplyDamage(mx, my, dam)
            'Если не умер, устанавливаем счетчик времени.
            If ret = FALSE Then
               _level.moninfo(midx).effects(meFire).cnt = spl.lvl
               _level.moninfo(midx).effects(meFire).dam = dam
            End If
            txt = "Fire Cloak Spell inflicted  " & dam & " damage to " & _level.moninfo(midx).mname & " for " & spl.lvl & " turns."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splBlind       'КнигаЗакл.: Ослепляет цель на lvl ходов
            txt = _level.moninfo(midx).mname & " is blinded for " & spl.lvl & " turns."
            _level.moninfo(midx).effects(meBlind).cnt = spl.lvl
            _level.moninfo(midx).effects(meBlind).dam = dam
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splFear        'КнигаЗакл.: Обращает монстров в бегство на lvl ходов.
            txt = _level.moninfo(midx).mname & " is filled with fear for " & spl.lvl & " turns."
            _level.moninfo(midx).effects(meFear).cnt = spl.lvl
            _level.moninfo(midx).effects(meFear).dam = dam
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splConfuse     'КнигаЗакл.: Оглушает монстра на lvl ходов.
            txt = _level.moninfo(midx).mname & " is confused for " & spl.lvl & " turns."
            _level.moninfo(midx).effects(meConfuse).cnt = spl.lvl
            _level.moninfo(midx).effects(meConfuse).dam = spl.lvl
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splFireBomb, splFireBall    'КнигаЗакл.: Повреждение по площади 20 (или 10) * lvl. Поджигает монстра на lvl ходов.
            'Проверим, возможно монстр уже в огне (предотвращает бесконечный цикл).
            If _level.moninfo(midx).effects(meFire).cnt < 1 Then
               ret = ApplyDamage(mx, my, dam * spl.lvl)
               'Если не умер, устанавливаем счетчик времени.
               If ret = FALSE Then
                  _level.moninfo(midx).effects(meFire).cnt = spl.lvl
                  _level.moninfo(midx).effects(meFire).dam = dam
               End If
               'Проверим тип заклинания.
               If spl.id = splFireBomb Then
                  txt = "Fire Bomb Spell inflicted  " & dam & " damage to " & _level.moninfo(midx).mname & "."
               Else
                  txt = "Fire Ball Spell inflicted  " & dam & " damage to " & _level.moninfo(midx).mname & "."
               End If
               PrintMessage txt
               'Будем вызывать рекурсивно, чтобы нанести повреждения рядом
               'стоящим монстрам. Это вызовет цепную реакцию, повреждая всех
               'монстров, которые стоят рядом друг с другом.
               For i As compass = north To nwest
                  'Установим начальную позицию.
                  vm.vx = mx
                  vm.vy = my
                  'Получим новую позицию.
                  vm += i
                  'Рекурсивно вызовем функцию.
                  tmp = ApplySpell(spl, vm.vx, vm.vy)
               Next
            Else
               txt = _level.moninfo(midx).mname & " is already on fire."
               PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
            End If
         Case splEntangle    'КнигаЗакл.: Обездвиживает монстра на lvel ходов и наносит  lvl повреждений каждый ход.
            ret = ApplyDamage(mx, my, dam)
            'Если не умер, устанавливаем счетчик времени.
            If ret = FALSE Then
               _level.moninfo(midx).effects(meEntangle).cnt = spl.lvl
               _level.moninfo(midx).effects(meEntangle).dam = dam
            End If
            txt = "Entangle Spell inflicted  " & dam & " damage to " & _level.moninfo(midx).mname & " for " & spl.lvl & " turns."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splCloudMind   'КнигаЗакл.: Цель не может пользоваться магией lvl ходов.
            txt = _level.moninfo(midx).mname & "mind is clouded for " & spl.lvl & " turns."
            _level.moninfo(midx).effects(meEntangle).cnt = spl.lvl
            _level.moninfo(midx).effects(meEntangle).dam = dam
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splIceStatue   'КнигаЗакл.: Замораживает цель на lvl ходов. Если цель заморожена, то может быть уничтожена с одного удара.
            _level.moninfo(midx).effects(meIceStatue).cnt = spl.lvl
            _level.moninfo(midx).effects(meIceStatue).dam = dam
            txt = _level.moninfo(midx).mname & " is frozen for " & spl.lvl & " turns."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splRust        'КнигаЗакл.: Уменьшает броню на lvl * 10%.
            pct = spl.lvl * .10
            _level.moninfo(midx).armval = _level.moninfo(midx).armval - pct
            If _level.moninfo(midx).armval < 0.0 Then
               _level.moninfo(midx).armval = 0.0
            Endif
            txt = _level.moninfo(midx).mname & " armor has been reduced."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splShatter     'КнигаЗакл.: Уничтожить оружие цели, если возможно.
            ret = ApplyDamage(mx, my, spl.lvl)
            'Если не умер, устанавливаем счетчик времени.
            If ret = FALSE Then
               _level.moninfo(midx).atkdam = 0
            End If
            txt = "Shatter Spell destroyed " & _level.moninfo(midx).mname & " attack ability."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splMagicDrain  'КнигаЗакл.: Уменьшает MDF цели на lvl% и добавляет заклинателю на 1 ход.
            _level.moninfo(midx).effects(meMDF).cnt = spl.lvl
            _level.moninfo(midx).effects(meMDF).dam = dam
            txt = "Magic Drain Spell has lowered " & _level.moninfo(midx).mname & " magic defense."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splPoison      'КнигаЗакл.: Отравляет цель по 1 HP на lvl ходов.
            ret = ApplyDamage(mx, my, dam)
            'Если не умер, устанавливаем счетчик времени.
            If ret = FALSE Then
               _level.moninfo(midx).effects(mePoison).cnt = spl.lvl
               _level.moninfo(midx).effects(mePoison).dam = dam
            Endif
            txt = "Poison Spell has poisoned " & _level.moninfo(midx).mname & " for " & spl.lvl & " turns."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splEnfeeble    'КнигаЗакл.: Уменьшает боевые факторы цели на lvl * 10%.
            _level.moninfo(midx).effects(meEnfeeble).cnt = spl.lvl
            _level.moninfo(midx).effects(meEnfeeble).dam = dam
            txt = "Enfeeble Spell has lowered " & _level.moninfo(midx).mname & " combat factors for " & spl.lvl & " turns."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splShout       'КнигаЗакл.: Оглушает всех видимых монстров на lvl ходов.
            For i As Integer = 1 To _level.nummon
               If _level.moninfo(i).isdead = FALSE Then
                  If  _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).visible = TRUE Then
                     _level.moninfo(i).effects(meStun).cnt = spl.lvl
                     _level.moninfo(i).effects(meStun).dam = dam
                     txt = "Warrior Shout Spell has stunned " & _level.moninfo(i).mname & " for " & spl.lvl & " turns."
                     PrintMessage txt
                  Endif
               End If
            Next
            _level.moninfo(midx).mcolor = fbMagenta
         Case splMindBlast   'КнигаЗакл.: Уменьшает MCF и MDF на lvl% на lvl ходов.
            _level.moninfo(midx).effects(meMDF).cnt = spl.lvl
            _level.moninfo(midx).effects(meMDF).dam = dam
            _level.moninfo(midx).effects(meMCF).cnt = spl.lvl
            _level.moninfo(midx).effects(meMCF).dam = dam
            txt = "Mind Blast Spell has lowered " & _level.moninfo(midx).mname & " magic magic combat factors."
            PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
         Case splTeleport
            'Нанесем достаточно повреждений, чтобы убить любого монстра.
            ret = ApplyDamage(mx, my, 1000000)
            txt = "You tleported into " & _level.moninfo(midx).mname & " killing it."
            PrintMessage txt
      End Select

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

monster.bi
'Описание типа для монстра.
  'Эффекты заклинаний, воздействующие на монстров.
  Enum monSpells
    mePoison      'Повреждение ядом.
    meFire        'Повреждение огнем.
    meStun        'Монстр оглушен.
    meAcidFog     '5 повреждений, продолжительностью lvl ходов
    meBlind       'Ослепление на lvl ходов
    meFear        'Обращает монстра в бегство на lvl ходов.
    meConfuse     'Дезориентировать монстра на lvl ходов.
    meEntangle    'Обездвижить монстра на level ходов и нанести lvl повреждений каждый ход.
    meCloudMind   'Уль не может пользоваться магией lvl ходов.
    meMagicDrain  'Уменьшить MDF цели на lvl% и добавить заклинателю на 1 ход.
    meEnfeeble    'Уменьшить боевые факторы цеди на lvl * 10%.
    meIceStatue   'Заморозить цель на level ходов.
    meMDF         'Уменьшить магическую защиту.
    meMCF         'Уменьшить магическую атаку.
  End Enum
   
  Type montype
  ...
    effects(mePoison To meMCF) As monSpellEffects 'Текущий эффект активирован на монстре.
  End Type

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

map.bi
'Обрабатывает все временные события.
  Sub levelobj.DoTimedEvents()
    Dim As String txt
    Dim As Integer tmp
     
    'Перебор всех монстров.
    For i As Integer = 1 To _level.nummon
      'Убедимся что монстр не мертв.
      If _level.moninfo(i).isdead = FALSE Then
         'Проверим все эффекты и назначим повреждения/состояния.
         If _level.moninfo(i).effects(mePoison).cnt > 0 Then
            tmp = ApplyDamage(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y, _level.moninfo(i).effects(mePoison).dam)
            _level.moninfo(i).effects(mePoison).cnt -= 1
         Else
            _level.moninfo(i).mcolor = fbRedBright
         Endif
  ...

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

Что произойдет, если монстр умрет от отравления? Мы можем это увидеть в функции ApplyDamage.

map.bi
'Назначает повреждения монстрам. Возвращает true если монстр умирает.
  Function levelobj.ApplyDamage(mx As Integer, my As Integer, dam As Integer) As Integer
    Dim As Integer midx, i, ret = FALSE
    Dim As vec v
    Dim As String txt
     
    'Убодимся что монстр здесь.
    If _level.lmap(mx, my).monidx > 0 Then
      midx = _level.lmap(mx, my).monidx
      _level.moninfo(midx).currhp = _level.moninfo(midx).currhp - dam
      'Если у монстро мало здоровья, он должен убегать.
      If _level.moninfo(midx).currhp < 2 Then _level.moninfo(midx).flee = TRUE
      'Проверим, возможно монстр должен умереть.
      If (_level.moninfo(midx).currhp < 1) Or (_level.moninfo(midx).effects(meIceStatue).cnt > 0) Then
         pchar.CurrXP = pchar.CurrXP + _level.moninfo(midx).xp
         'Монстр мертв.
         ret = TRUE
         'Установим флаг смерти монстра.
         _level.moninfo(midx).isdead = TRUE
         'Удалим монстра с карты.
         _level.lmap(mx, my).monidx = 0
         'Выбросим предметы.
         If _level.moninfo(midx).dropcount > 0 Then
            For i = 1 To _level.moninfo(midx).dropcount
               For j As compass = north To nwest
                  v.vx = mx
                  v.vy = my
                  v += j
                  'Если на полу ничего нет.
                  If (_level.lmap(v.vx, v.vy).terrid = tFloor) And (_level.linv(v.vx, v.vy).classid = clNone) Then
                     PutItemOnMap v.vx, v.vy, _level.moninfo(midx).dropitem(i)
                     Exit For
                  Endif
               Next
               ClearInv _level.moninfo(midx).dropitem(i)
            Next
         Endif
      Endif
     'Отобразим результат боя.
      If _level.moninfo(midx).isdead = TRUE Then
         txt = pchar.CharName & " killed the " & _level.moninfo(midx).mname & " with " & dam & " damage points."
      Else
         txt = pchar.CharName & " hit the " & _level.moninfo(midx).mname & " for " & dam & " damage points."
      Endif
      PrintMessage txt
    Endif
     
    Return ret
  End Function

Если монстр умирает, то мы добавляем персонажу опыт (pchar.CurrXP = pchar.CurrXP + _level.moninfo(midx).xp) и выбрасываем на землю предметы из инвентаря монстра. Мы вызываем эту функцию не только из ApplySpell и DoTimedEvents, но и в процедурах ближнего и дистанционного боя в dod.bas. Снова повторюсь: повторное использование кода — наш друг.

Есть два заклинания в ApplySpell, которые требуют детального рассмотрения.

map.bi
Case splFireBomb, splFireBall    'КнигаЗакл.: Повреждение по площади 20 (или 10) * lvl. Поджигает монстра на lvl ходов.
            'Убедимся, что монстр уже не горит (для исключения бесконечного цикла).
            If _level.moninfo(midx).effects(meFire).cnt < 1 Then
               ret = ApplyDamage(mx, my, dam * spl.lvl)
               'Если не умер, установим счетчик времени (ходов).
               If ret = FALSE Then
                  _level.moninfo(midx).effects(meFire).cnt = spl.lvl
                  _level.moninfo(midx).effects(meFire).dam = dam
               End If
               'Проверим тип заклинания.
               If spl.id = splFireBomb Then
                  txt = "Fire Bomb Spell inflicted  " & dam & " damage to " & _level.moninfo(midx).mname & "."
               Else
                  txt = "Fire Ball Spell inflicted  " & dam & " damage to " & _level.moninfo(midx).mname & "."
               End If
               PrintMessage txt
               'Будем вызывать рекурсивно, чтобы нанести повреждения рядом
               'стоящим монстрам. Это вызовет цепную реакцию, повреждая всех
               'монстров, которые стоят рядом друг с другом.
               For i As compass = north To nwest
                  'Установим начальную позицию.
                  vm.vx = mx
                  vm.vy = my
                  'Получим новую позицию.
                  vm += i
                  'Рекурсивно вызовем функцию снова.
                  tmp = ApplySpell(spl, vm.vx, vm.vy)
               Next
            Else
               txt = _level.moninfo(midx).mname & " is already on fire."
               PrintMessage txt
            _level.moninfo(midx).mcolor = fbMagenta
            End If

Тут у нас обрабатываются заклинания «Огненный Шар» и «Огненная Бомба». Так как оба заклинания ведут себя одинаково, то и реализацию этих заклинаний мы можем описать в одном месте. В отличии от других заклинаний, которые воздействуют только на одного монстра, эти заклинания наносят дополнительный ущерб. Если два монстра стоят рядом, то будут поражены оба монстра. Мы добиваемся этого рекурсивным вызовом функции ApplySpell.

Рекурсия — мощное средство, и позволяет упростить многие задачи в программировании. Здесь мы проверяем каждую клетку карты вокруг цели заклинания, и применяем его эффект на эти клетки. Если на одной из этих клеток окажется монстр, то он будет атакован заклинанием, как будто он был целью. Затем мы рассмотрим каждый квадрат вокруг нового монстра и так далее. При использовании рекурсии, вы должны убедиться, что у вас есть условие выхода из нее, иначе программа будет снова и снова вызывать одну и туже функцию из самой себя, пока стек не переполниться и программа не «упадет». У нас имеется два условия выхода: если монстра нет на проверяемой ячейке карты, и если монстр уже подожжен. Последнее условие наиболее важно. Если мы будем все время поджигать уже горящих монстров, то цикл никогда не завершиться, что равносильно сбою программы. Проверка обоих условий обеспечит нам безопасный выход из рекурсии.

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

dod.bas:CastSpell
Select Case sinv.spell.id
    Case splHeal
      tmp = pchar.MaxHP * (sinv.spell.lvl / 100)
      If tmp < 1 Then tmp = 1
      pchar.CurrHP = pchar.CurrHP + tmp
    Case splMana
      tmp = pchar.MaxMana * (sinv.spell.lvl / 100)
      If tmp < 1 Then tmp = 1
      pchar.CurrMana = pchar.CurrMana + tmp
      cancel = TRUE

Восстановление здоровья и восстановление маны делают именно то, что написано у них в названии — восстанавливают здоровье и ману соответственно. Разница в том, что для заклинания восстановления маны не требуется мана. Мы добиваемся этого, используя флаг cancel. Когда мы доберемся до расчета потраченной маны, вы увидите как этот флаг работает.

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

dod.bas:CastSpell
Case splRecharge
    'Поищем жезлы в инвентаря и перезарядим в соответствии с уровнем.
    For i = pchar.LowInv To pchar.HighInv
      iitem = pchar.HasInvItem(i)
      If iitem = TRUE Then
         'Получим предмет инвентаря.
         pchar.GetInventoryItem i, iinv
         'Проверим что это оружие.
         If iinv.classid = clWeapon Then
            'Убедимся что это жезл.
            If iinv.weapon.iswand = TRUE Then
               'Увеличим кол-во зарядов.
               iinv.weapon.ammocnt += sinv.spell.lvl
               'Убедимся, что не превысили максимальный уровень зарядов.
               If iinv.weapon.ammocnt > iinv.weapon.capacity Then
                  iinv.weapon.ammocnt = iinv.weapon.capacity
               Endif
               'Вернем предмет в инвентарь.
               pchar.AddInvItem iitem, iinv
            Endif
         Endif
      End If
    Next
    'Проверим главный слот экипировки оружия.
    iitem = pchar.HasInvItem(wPrimary)
    If iitem = TRUE Then
      pchar.GetInventoryItem wPrimary, iinv
      If iinv.classid = clWeapon Then
         'Убедимся что это жезл.
         If iinv.weapon.iswand = TRUE Then
            iinv.weapon.ammocnt += sinv.spell.lvl
            If iinv.weapon.ammocnt > iinv.weapon.capacity Then
               iinv.weapon.ammocnt = iinv.weapon.capacity
            Endif
            pchar.AddInvItem wPrimary, iinv
         Endif
      Endif
    Endif
    'Проверим второй слот экипировки оружия.
    iitem = pchar.HasInvItem(wSecondary)
    If iitem = TRUE Then
      pchar.GetInventoryItem wSecondary, iinv
      If iinv.classid = clWeapon Then
         'Убедимся что это жезл.
         If iinv.weapon.iswand = TRUE Then
            iinv.weapon.ammocnt += sinv.spell.lvl
            If iinv.weapon.ammocnt > iinv.weapon.capacity Then
               iinv.weapon.ammocnt = iinv.weapon.capacity
            Endif
            pchar.AddInvItem wSecondary, iinv
         Endif
      Endif
    Endif

В первом For-Next цикле мы проверяем все предметы инвентаря, чтобы найти жезлы. Если жезл найден, то мы увеличиваем количество его зарядов в соответствии с уровнем заклинания. Так как жезл нельзя зарядить большим количеством зарядов чем максимально возможное для данного жезла, то мы должны проверить максимальный уровень и уменьшить кол-во зарядов, если оно превышает максимально допустимое значение. Заряженный жезл мы возвращаем в тот же слот инвентаря.

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

dod.bas:CastSpell
Case splFocus
    'Увеличивает все боевые факторы на 1 ход.
    pchar.BonUcf = sinv.spell.lvl
    pchar.BonUcfCnt = 1
    pchar.BonAcf = sinv.spell.lvl
    pchar.BonAcfCnt = 1
    pchar.BonPcf = sinv.spell.lvl
    pchar.BonPcfCnt = 1
    pchar.BonCdf = sinv.spell.lvl
    pchar.BonCdfCnt = 1
    pchar.BonMcf = sinv.spell.lvl
    pchar.BonMcfCnt = 1
    pchar.BonMdf = sinv.spell.lvl
    pchar.BonMdfCnt = 1

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

dod.bas:CastSpell
Case splTeleport
    'Получим координаты цели.
    ret = GetTargetCoord(vt, sinv.spell.lvl)
    If ret = TRUE Then
      'Проверим, возможно в координатах находится монстр.
      If level.IsMonster(vt.vx, vt.vy) = TRUE Then
         'Телепорт в монстра убивает его.
         ret = level.ApplySpell(sinv.spell, vt.vx, vt.vy)
         'Установим новую позицию персонажа.
         pchar.Locx = vt.vx
         pchar.Locy = vt.vy
         'Сгенерируем карту звука.
         level.ClearSoundMap
         snd = pchar.GetNoise()
         level.GenSoundMap(pchar.Locx, pchar.Locy, snd)
      Else
         'Убедимся, что позиция не заблокирована.
         If level.IsBlocking(vt.vx, vt.vy) = FALSE Then
            'Установим новую позицию персонажа.
            pchar.Locx = vt.vx
            pchar.Locy = vt.vy
            'Сгенерируем карту звука.
            level.ClearSoundMap
            snd = pchar.GetNoise()
            level.GenSoundMap(pchar.Locx, pchar.Locy, snd)
         Else
            ShowMsg "Teleport", "You can't teleport there.", tWidgets.MsgBoxType.gmbOK
            cancel = TRUE
         End If  
      Endif
    Else
      cancel = TRUE
    Endif

Заклинание телепортации позволяет выбрать место, куда персонаж должен перенестись. Для указания координат места, мы вызываем функцию GetTargetCoord. Обратите внимание, что если персонаж телепортируется в монстра, то это должно убить его, поэтому мы вызываем тут ApplySpell, хотя, технически, телепортация это не заклинание атаки.

Как только игрок выбирает верное место для телепортации (ячейка пола а не стену), мы перемещаем его в данную локацию, также, как будто он туда пришел пешком. Дополнительно мы генерируем необходимые данные, например карту звука. Также, мы используем здесь флаг cancel для отмены расхода маны, если игрок решил не телепортироваться, уже выбрав заклинание телепортации.

Следующее заклинание, которое мы рассмотрим, это заклинание отпирания дверей.

dod.bas:CastSpell
Case splOpen
    'Получим координаты цели.
    ret = GetTargetCoord(vt, sinv.spell.lvl)
    If ret = TRUE Then
      'Получим идентификатор местности.
      tid = level.GetTileID(vt.vx, vt.vy)
      'Проверим, что это закрытая дверь.
      If tid = tDoorClosed Then
         'Убедимся что она заперта.
         If level.IsDoorLocked(vt.vx, vt.vy) = TRUE Then
            ret = level.OpenLockedDoor(vt.vx, vt.vy, sinv.spell.lvl)
            If ret = TRUE Then
               PrintMessage "Door was opened."
            Endif
         Else
            'Сообщим игроку, что дверь не заперта.
            ShowMsg "Open Spell", "The door is not locked.", tWidgets.MsgBoxType.gmbOK
            cancel = TRUE
         Endif
      Endif
    Else
      cancel = TRUE
    End If

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

level.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
        Else
           'Попытка провалилась.
           ret = FALSE
        Endif
     Endif
    Endif
    
    Return ret
  End Function

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

Последнее заклинание, действующее на персонажа, это «случайная телепортация».

dod.bas:CastSpell
Case splBlink
    'Установим эффект заклинания мерцания.
    pchar.SetSpellEffect sinv.spell.id, sinv.spell.lvl, 0

Изначально, заклинание мерцания, это должна была быть случайная телепортация, однако, поскольку у нас уже есть заклинания телепортации, я решил поступить с мерцанием как то иначе. Обратите внимание, мы просто вызываем SetSpellEffect передавая в него идентификатор нашего заклинания.

character.bi
'Установим эффект заклинания.
  Sub character.SetSpellEffect(splid As cspleffects, scnt As Integer, samt As Integer)
    Select Case splid
      Case cPoison
         _cinfo.cseffect(cPoison).cnt = scnt
         _cinfo.cseffect(cPoison).amt = samt
      Case cBlink
         _cinfo.cseffect(cBlink).cnt = scnt
         _cinfo.cseffect(cBlink).amt = samt
    End Select
  End Sub

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

character.bi
'Эффекты заклинаний.
  Enum cspleffects
    cPoison
    cBlink
  End Enum
   
  'Описание типа эффекта.
  Type cspleftype
    cnt As Integer    'Продолжительность.
    amt As Integer    'Мощность эффекта.
  End Type
 
  'Определение типа атрибутов персонажа.
  Type characterinfo
  ...
    cseffect(cPoison To cBlink) As cspleftype 'Массив эффектов заклинаний.
  End Type

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

character.bi
'Управление всеми продолжительными эффектами.
  Sub character.DoTimedEvents()
    Dim As Integer roll1, roll2, v1, v2, amt, statamt
     
    'Яд наносит повреждения персонажу, в зависимости от силы отравления.
    If Poisoned = TRUE Then
      'Получим силу яда.
      v1 = PoisonStr
      'Получим выносливость персонажа + бонус
      v2 = CurrSta + BonSta
      'Возьмем случайные значения.
      roll1 = RandomRange(1, v1)
      roll2 = RandomRange(1, v2)
      'Если яд выиграл,
      If roll1 > roll2 Then
         'Отнимем единицу здоровья.
         CurrHP = CurrHP - 1
      Endif
    Endif
    'Проверим счетчики бонусов и применим необходимые бонусы.
    'Сила.
    If BonStrCnt > 0 Then
      BonStrCnt = BonStrCnt - 1
      If BonStrCnt < 1 Then
         BonStr = 0
      Endif
    Endif
    'Выносливость
    If BonStaCnt > 0 Then
      BonStaCnt = BonStaCnt - 1
      If BonStaCnt < 1 Then
         BonSta = 0
      Endif
    Endif
    'Ловкость.
    If BonDexCnt > 0 Then
      BonDexCnt = BonDexCnt - 1
      If BonDexCnt < 1 Then
         BonDex = 0
      Endif
    Endif
    'Подвижность
    If BonAglCnt > 0 Then
      BonAglCnt = BonAglCnt - 1
      If BonAglCnt < 1 Then
         BonAgl = 0
      Endif
    Endif
    'Интеллект.
    If BonIntCnt > 0 Then
      BonIntCnt = BonIntCnt - 1
      If BonIntCnt < 1 Then
         BonInt = 0
      Endif
      charint = _cinfo.intatt(idxAttr) + BonInt
    Endif
    'Безоружный бой.
    If BonUcfCnt > 0 Then
      BonUcfCnt = BonUcfCnt - 1
      If BonUcfCnt < 1 Then
         BonUcf = 0
      Endif
    Endif
    'Ближний Бой с оружием.
    If BonAcfCnt > 0 Then
      BonAcfCnt = BonAcfCnt - 1
      If BonAcfCnt < 1 Then
         BonAcf = 0
      Endif
    Endif
    'Дистанционный бой.
    If BonPcfCnt > 0 Then
      BonPcfCnt = BonPcfCnt - 1
      If BonPcfCnt < 1 Then
         BonPcf = 0
      Endif
    Endif
    'Магический бой.
    If BonMcfCnt > 0 Then
      BonMcfCnt = BonMcfCnt - 1
      If BonMcfCnt < 1 Then
         BonMcf = 0
      Endif
    Endif
    'Защита.
    If BonCdfCnt > 0 Then
      BonCdfCnt = BonCdfCnt - 1
      If BonCdfCnt < 1 Then
         BonCdf = 0
      Endif
    Endif
    'Магическая защита.
    If BonMdfCnt > 0 Then
      BonMdfCnt = BonMdfCnt - 1
      If BonMdfCnt < 1 Then
         BonMdf = 0
      Endif
    Endif
    'Проверим ожерелья и кольца.
    statamt = MaxHP
    amt = GetJewleryEffect(jwRegenHP, statamt)
    CurrHP = CurrHP + amt
    statamt = MaxMana
    amt = GetJewleryEffect(jwRegenMana, statamt)
    CurrMana = CurrMana + amt
    'Проверим заклинание мерцания.
    If _cinfo.cseffect(cBlink).cnt > 0 Then
      _cinfo.cseffect(cBlink).cnt = _cinfo.cseffect(cBlink).cnt - 1
      If _cinfo.cseffect(cBlink).cnt < 0 Then _cinfo.cseffect(cBlink).cnt = 0
    Endif
  End Sub

Здесь вы можете видеть два изменения. Первое — для эффекта отравление теперь используется массив состояний, а также, в последней части кода, добавлена проверка и уменьшение счетчика для эффекта случайного телепорта. Но где же сама реализация этого эффекта? Давайте взглянем на код атаки монстров.

level.bi
'Монстр атакует персонажа.
  Sub levelobj.MonsterAttack(mx As Integer, my As Integer)
    Dim As Integer midx, cd, mc, rollc, rollm, chp, dam
    Dim As String txt
    Dim As Single arm
     
    'Убедимся что монстр присутствует.
    If (_level.lmap(mx, my).monidx > 0) And (pchar.BlinkActive = FALSE) Then
    ...

Если флаг заклинания мерцания активен, то персонаж «невидим» для монстра в этом раунде, и монстр не может его атаковать. Свойство объекта персонажа BlinkActive возвращает текущее состояние флага заклинания мерцания.

character.bi
'Возврашает TRUE если заклинание мерцания активно.
  Property character.BlinkActive() As Integer
    Return (_cinfo.cseffect(cBlink).cnt > 0)
  End Property

Предыдущий код может выглядеть несколько странно, если вы не видели этой техники раньше. Мы при помощи оператора > проверяем количество оставшихся ходов для действия заклинания мерцания. Так как оператор > это внутренняя функция, которая возвращает True (-1) или False (0), то мы можем воспользоваться этим для получения нашего возвращаемого значения. Это быстрый и эффективный метод для проверки значения переменной.

Оставшаяся часть кода в CastSpell следит за вычитанием необходимого количества маны после использования заклинания.

dod.bas:CastSpell
...
    End Select
  Endif
  If cancel = FALSE Then
    'Отнимем необходимое кол-во маны.
    pchar.CurrMana = pchar.CurrMana - sinv.spell.manacost
  End If
  ...

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

Мы рассмотрели почти весь код для реализации заклинаний, однако, существуют некоторые заклинания, например Заморозки, или Дезориентации, которые влияют на передвижение монстров, поэтому му должны внести изменения в код процедуры MoveMonsters.

level.bi
'Перемещение всех дивых монстров.
  Sub levelobj.MoveMonsters ()
    Dim As mcoord nxt
    Dim As Integer pdist
           
    'Переберем всех монстров.
    For i As Integer = 1 To _level.nummon
      'Убедимся, что монстр жив.
      If (_level.moninfo(i).isdead = FALSE) And _
         (_level.moninfo(i).effects(meStun).cnt < 1) And _
         (_level.moninfo(i).effects(meBlind).cnt < 1) And _
         (_level.moninfo(i).effects(meEntangle).cnt < 1) And _
         (_level.moninfo(i).effects(meIceStatue).cnt < 1) And _
         (_level.moninfo(i).effects(meConfuse).cnt < 1) Then
         'Монстр убегает?
         If _level.moninfo(i).flee = FALSE Then
  ...

Тут мы проверяем все заклинания, которые влияют на движение монстров. Некоторые из них, наносят монстрам урон, как например, заклинание «Спутать». Это дает персонажу нанести дополнительные «бесплатные» повреждения монстрам, что делает эти заклинания весьма ценными. Заклинание «Ледяная Статуя» также дает дополнительную возможность убить монстра с одного удара, когда он под его воздействием.

level.bi
'Наносит повреждения монстрам, возвращает true если монстр умирает.
  Function levelobj.ApplyDamage(mx As Integer, my As Integer, dam As Integer) As Integer
    Dim As Integer midx, i, ret = FALSE
    Dim As vec v
    Dim As String txt
     
    'Убедимся что монстр здесь.
    If _level.lmap(mx, my).monidx > 0 Then
      midx = _level.lmap(mx, my).monidx
      _level.moninfo(midx).currhp = _level.moninfo(midx).currhp - dam
      'Проверим, возможно монстру нужно убегать.
      If _level.moninfo(midx).currhp < 2 Then _level.moninfo(midx).flee = TRUE
      'Проверим, умер ли монстр.
      If (_level.moninfo(midx).currhp < 1) Or (_level.moninfo(midx).effects(meIceStatue).cnt > 0) Then
         pchar.CurrXP = pchar.CurrXP + _level.moninfo(midx).xp
         'Монстр умер.
         ret = TRUE
         'Установим флаг, указывающий что монстр мертв.
         _level.moninfo(midx).isdead = TRUE
...

Здесь мы видим, что если заклинание «Ледяная Статуя» активно, то монстр, при получении повреждений, сразу же умирает. Размещая код заклинания здесь, нам не нужно беспокоится о процедурах боя. Если, в какой то момент, мы решим добавить новые боевые режимы, то заклинание будет охватывать и их тоже.

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

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

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