Давайте сделаем рогалик. Глава 29: Магия оружия

Материал из Клуб любителей рогаликов
Перейти к: навигация, поиск
Richard D Clark-Let's Build a WeaponSpell.png

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

inv.bi

'Идентификаторы эффектов.
 Enum spellid
   splNone        'Нет эффекта.
   splMaxHealing  'Восстанавливает здоровье до максимума.
   splStrongMeat  'Добавляет временный бонус к силе.
   splBreadLife   'Лечит отравление.
   splMaxMana     'Полностью восстанавливает ману.
   splSerpentBite 'Оружие: Наносит урон ядом.
   splRend        'Оружие: Уменьшает броню цели.
   splSunder      'Оружие: Снижает наносимый целью урон оружием.
   splReaper      'Оружие: Вызывает у монстра приступ страха.
   splFire        'Оружие: Поджигает цель.
   splGoliath     'Оружие: Добавляет к атаке дополнительную силу.
   splStun        'Оружие: Обездвиживает цель на время.
   splChaos       'Оружие: Добавляет случайные повреждения.
   splWraith      'Оружие: Уменьшает случайную характеристику цели.
  splThief      'Оружие: Забирает случайное количество от параметра цели и передает персонажу.
 End Enum

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

inv.bi

'Определение типа информации о заклинании.
 Type spelltype
   id As spellid          'Идентификатор заклинания.
   lvl As Integer         'Уровень заклинания.
   splname As String * 30 'Название заклинания.
   spldesc As String * 60 'Описание заклинания.
   manacost As Integer    'Стоимость маны.
   dam As Integer         'Кол-во возможных повреждений, наносимых заклинанием.
 End Type

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

Поле id содержит идентификатор заклинания из перечисления spellid. Поле lvl содержит уровень заклинания. Чем выше уровень заклинания, тем сильнее его эффект. Поля splname и spldesc используются подпрограммой работы с инвентарем персонажа и содержат название заклинания и его описание. Поле manacost указывает на количество маны, необходимой для использования данного заклинания. Мы не будем использовать данное поле для заклинаний оружия, так как данный тип заклинаний — это часть самого оружия и не требует чего либо еще, однако количество маны будет играть значительную роль при использовании заклинаний самим персонажем. Поле dam содержит количество урона, наносимого данным заклинанием, но, так как не все заклинания наносят урон, то мы будем использовать это поле и для других целей.

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

inv.bi

'Определение типа для оружия.
Type weapontype
  id As weaponids           'Тип оружия
  evaldr As Integer         'Сложность определения. Evaldr > 0 для маг. предметов.
  eval As Integer           'Определен ли предмет.
  spell As spelltype        'Заклинание.
  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        'Кол-во снарядов в обойме.
  iswand As Integer         'Если это «волшебная палочка».
End Type

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

inv.bi

'Создание оружия.
 Sub GenerateWeapon(inv As invtype, currlevel As Integer, wpid As weaponids = wpNone)
   Dim item As weaponids
   Dim As Integer isMagic
  
   isMagic = ItemIsMagic(currlevel)
   item = wpid
   'Создадим тип, если не задан.
   If item = wpNone Then
     item = RandomRange(wpClub, wpGoldWand)
   EndIf
   'Общее для всех предметов.
   inv.classid = clWeapon
   inv.weapon.id = item
   inv.iconclr = fbCadmiumYellow
   inv.weapon.use = useWieldWear
   inv.weapon.eval = FALSE
   inv.weapon.wslot(1) = wPrimary
   inv.weapon.wslot(2) = wSecondary
   inv.weapon.iswand = FALSE
   ClearSpell inv.weapon.spell
   'Магический предмет.
   If IsMagic = TRUE Then
     inv.weapon.evaldr = GetScaledFactor(charint, currlevel) 'Сложность опознания.
     inv.weapon.spell.id = RandomRange(splSerpentBite, splThief) 'Идентификатор заклинания.
     GenerateSpell inv.weapon.spell, currlevel
   EndIf
   'Зададим значения по умолчанию для типа оружия и боеприпасов.
   inv.weapon.weapontype = wtMelee
   inv.weapon.ammotype =  amNone
   inv.weapon.ammocnt = 0
   'Зададим тип оружия и количество.
   Select Case item
 ...

Как видите, нам пришлось совсем немного ее изменить для поддержки нового типа заклинаний. Хотя мы и добавили несколько новых подпрограмм. Подпрограмма ClearSpell просто очищает структуру данных заклинания. GenerateSpell заполняет структуру данных заклинания в соответствии с типом заклинания выбранного при помощи функции RandomRange. Здесь вы можете увидеть, почему мы хотим, чтобы идентификаторы заклинаний для оружия находились в одном месте. При добавлении нового заклинания, мы добавим его идентификатор между существующими идентификаторами задающими диапазон заклинаний, что позволит нам обойтись без изменения существующего кода в GenerateWeapon. Необходимо будет только добавить код для нового заклинания в процедуру GenerateSpell. Теперь давайте посмотрим на две новых подпрограммы для создания заклинаний.

inv.bi

'Очийает структуру заклиания
Sub ClearSpell (spl As spelltype)
  'Clear the spell type.
  spl.id = splNone
  spl.lvl = 0
  spl.splname = ""
  spl.spldesc = ""
  spl.manacost = 0
  spl.dam = 0
End Sub

Процедура ClearSpell просто очищает структуру spelltype. Обратите внимание, что мы передаем в процедуру именно только те данные, которые необходимо очистить, а не всю структуру данных оружия. Это позволит нам использовать данную процедуру не только для оружия, но и для заклинаний других предметов, в также позволит избежать случайных изменений данных, которая данная процедура изменять не должна.

inv.bi

'Заполняем данные заклинаний.
Sub GenerateSpell(spl As spelltype, currlevel As Integer)
   
  'Создаем заклинание основывая на его идентификаторе.
  Select Case spl.id
     Case splSerpentBite 'Оружие: Наносит урон ядом.
        spl.lvl = currlevel 'Current level is used for spell level.
        spl.splname = GetSpellName(spl.id) 'Sets the spell name.
        spl.spldesc = GetSpellEffect(spl.id) 'Sets the spell description.
        spl.manacost = 0 'Cost in mana.
        spl.dam = currlevel 'How much damage the spell does.
     Case splRend        'Оружие: Уменьшает броню цели.
        spl.lvl = currlevel
        spl.splname = GetSpellName(spl.id)
        spl.spldesc = GetSpellEffect(spl.id)
        spl.manacost = 0
        spl.dam = currlevel
     Case splSunder      'Оружие: Снижает наносимый целью урон оружием.
        spl.lvl = currlevel
        spl.splname = GetSpellName(spl.id)
        spl.spldesc = GetSpellEffect(spl.id)
        spl.manacost = 0
        spl.dam = currlevel
     Case splReaper      'Оружие: Вызывает у монстра приступ страха.
        spl.lvl = currlevel
        spl.splname = GetSpellName(spl.id)
        spl.spldesc = GetSpellEffect(spl.id)
        spl.manacost = 0
        spl.dam = currlevel
     Case splFire        'Оружие: Поджигает цель.
        spl.lvl = currlevel
        spl.splname = GetSpellName(spl.id)
        spl.spldesc = GetSpellEffect(spl.id)
        spl.manacost = 0
        spl.dam = currlevel
     Case splGoliath     'Оружие: Добавляет к атаке дополнительную силу.
        spl.lvl = currlevel
        spl.splname = GetSpellName(spl.id)
        spl.spldesc = GetSpellEffect(spl.id)
        spl.manacost = 0
        spl.dam = currlevel
     Case splStun        'Оружие: Обездвиживает цель на время.
        spl.lvl = currlevel
        spl.splname = GetSpellName(spl.id)
        spl.spldesc = GetSpellEffect(spl.id)
        spl.manacost = 0
        spl.dam = currlevel
     Case splChaos       'Оружие: Добавляет случайные повреждения.
        spl.lvl = currlevel
        spl.splname = GetSpellName(spl.id)
        spl.spldesc = GetSpellEffect(spl.id)
        spl.manacost = 0
        spl.dam = RandomRange(1, currlevel)
     Case splWraith      'Оружие: Уменьшает случайную характеристику цели.
        spl.lvl = currlevel
        spl.splname = GetSpellName(spl.id)
        spl.spldesc = GetSpellEffect(spl.id)
        spl.manacost = 0
        spl.dam = currlevel
     Case splThief       'Оружие: Забирает случайное количество от параметра цели и передает персонажу.
        spl.lvl = currlevel
        spl.splname = GetSpellName(spl.id)
        spl.spldesc = GetSpellEffect(spl.id)
        spl.manacost = 0
        spl.dam = currlevel
     Case Else
        spl.lvl = 0
        spl.splname = "Unknown spell"
        spl.spldesc = "Unknown spell effect."
        spl.manacost = 0 'Cost in mana.
        spl.dam = 0
  End Select
End Sub

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

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

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

inv.bi

'Возвращает название заклинания.
 Function GetSpellName(spl As spellid) As String
   Dim As String ret = ""
   
   Select Case spl
     Case splMaxHealing  'Восстанавливает здоровье до максимума.
        ret = "Spell of Maximum healing."
     Case splStrongMeat  'Добавляет временный бонус к силе.
        ret = "Spell of Enhance Strength."
     Case splBreadLife   'Лечит отравление.
        ret = "Spell of Cure Poison."
     Case splMaxMana     'Полностью восстанавливает ману.
        ret = "Spell of Restore Mana."
     Case splSerpentBite 'Оружие: Наносит урон ядом.
        ret = "Serpent's Bite"
     Case splRend        'Оружие: Уменьшает броню цели.
        ret = "Spell of Rend Armor"
     Case splSunder      'Оружие: Снижает наносимый целью урон оружием.
        ret = "Spell of Sunder"
     Case splReaper      'Оружие: Вызывает у монстра приступ страха.
        ret = "Spell of The Reaper"
     Case splFire        'Оружие: Поджигает цель.
        ret = "Spell of Fire"
     Case splGoliath     'Оружие: Добавляет к атаке дополнительную силу.
        ret = "The Strength of Goliath"
     Case splStun        'Оружие: Обездвиживает цель на время.
        ret = "Spell of Stun"
     Case splChaos       'Оружие: Добавляет случайные повреждения.
        ret = "Chaos Attack"
     Case splWraith      'Оружие: Уменьшает случайную характеристику цели.
        ret = "Hand of the Wraith"
     Case splThief       'Оружие: Забирает случайное количество от параметра цели и передает персонажу.
        ret = "Spell of The Phantom"             
     Case Else
        ret = "No spell."
   End Select
  
   Return ret
 End Function

Эта функция просто смотрит на id заклинания и возвращает имя, связанное с этим идентификатором. Ничего сложного.

inv.bi

'Возвращает эффект заклиания. Используется в описании.
 Function GetSpellEffect(spl As spellid) As String
   Dim As String ret = ""
   
   Select Case spl
     Case splMaxHealing  'Восстанавливает здоровье до максимума.
        ret = "Restore full health."
     Case splStrongMeat 'Добавляет временный бонус к силе.
        ret = "Adds bonus to strength."
     Case splBreadLife   'Лечит отравление.
        ret = "Cures poison."
     Case splMaxMana     'Полностью восстанавливает ману.
        ret = "Restore full mana."
     Case splSerpentBite 'Оружие: Наносит урон ядом.
        ret = "Inflicts poison damage over time."
     Case splRend        'Оружие: Уменьшает броню цели.
        ret = "Decreases armor of target."
     Case splSunder      'Оружие: Снижает наносимый целью урон оружием.
        ret = "Decreases target weapon damage."
     Case splReaper      'Оружие: Вызывает у монстра приступ страха.
        ret = "Causes monster to flee."
     Case splFire        'Оружие: Поджигает цель.
        ret = "Inflicts fire damage over time."
     Case splGoliath     'Оружие: Добавляет к атаке дополнительную силу.
        ret = "Adds additional strength to damage."
     Case splStun        'Оружие: Обездвиживает цель на время.
        ret = "Stuns target doing damage for a time."
     Case splChaos       'Оружие: Добавляет случайные повреждения.
        ret = "Adds random damage to attack."
     Case splWraith      'Оружие: Уменьшает случайную характеристику цели.
        ret = "Decreases random attribute of target."
     Case splThief       'Оружие: Забирает случайное количество от параметра цели и передает персонажу.
        ret = "Steals random attribute amount and adds to character."             
     Case Else
        ret = "No effect."
   End Select
   
   Return ret
 End Function

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

Давайте посмотрим, как мы показываем эту информацию игроку.

inv.bi

'Возврайает подробное описание предмета.
 Sub GetFullDesc(lines() As String, inv As invtype)
   Dim As Integer idx = 0
   
   'Очистим массив.
   ReDim lines(0 To idx) As String
   'Убедимся, что есть что инспектировать.
   If inv.classid <> clNone Then
     'Выберем предмет.
     Select Case inv.classid
        ...

        Case clWeapon
           idx += 1
           ReDim Preserve lines(0 to idx) As String
           lines(idx) = GetInvItemDesc(inv)
           idx += 1
           ReDim Preserve lines(0 to idx) As String
           lines(idx) = "* " & inv.weapon.dam & " weapon damage"
           idx += 1
           ReDim Preserve lines(0 to idx) As String
           lines(idx) = "* Hands Required: " & inv.weapon.hands
           If inv.weapon.weapontype = wtProjectile Then
              idx += 1
              ReDim Preserve lines(0 to idx) As String
              lines(idx) = "* Capacity: " & inv.weapon.capacity
              idx += 1
              ReDim Preserve lines(0 to idx) As String
              lines(idx) = "* Ammo Cnt: " & inv.weapon.ammocnt
           EndIf
           If IsEval(inv) = TRUE Then
              If inv.weapon.spell.id <> splNone Then
                 idx += 1
                 ReDim Preserve lines(0 to idx) As String
                 lines(idx) = "* Spell: " & Trim(inv.weapon.spell.splname)
                 idx += 1
                 ReDim Preserve lines(0 to idx) As String
                 lines(idx) = "* Effect: " & Trim(inv.weapon.spell.spldesc)
              End If
              idx += 1
              ReDim Preserve lines(0 to idx) As String
              lines(idx) = "* Item is evaluated"
           Else
              idx += 1
              ReDim Preserve lines(0 to idx) As String
              lines(idx) = "* Item is not evaluated"
           End If

           ...

     End Select
   EndIf
 End Sub

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

dod.bas

'Расчет ближнего боя.
 Sub DoMeleeCombat(mx As Integer, my As Integer)
   Dim As Integer cf, df, croll, mroll, dam, isdead, midx, mxp, xp
   Dim As String txt, mname
   Dim As Single marm
   Dim As spelltype spl1, spl2
   
   'Поучим фактор защиты монстра.
   df = level.GetMonsterDefense(mx, my)
   mname = level.GetMonsterName(mx, my)
   'Получим здоровье монстра.
   mxp = level.GetMonsterXP(mx, my)
   'Убедимся что есть данные.
   If df > 0 Then
     'Возвращает фактор атаки основанный на том, чем вооружен персонаж.
     cf = pchar.GetMeleeCombatFactor()
     'Возьмем случайные значения.
     croll = RandomRange(1, cf)
     mroll = RandomRange(1, df)
     'Если персонаж ударил монстра.     
     If croll > mroll Then
        'Получим наносимое оружием повреждение.
        dam = pchar.GetWeaponDamage()
        'Получим броню монстра.
        marm = level.GetMonsterArmor(mx, my)
        'Рассчитаем повреждения, основанное на рейтинге брони.
        dam = dam - (dam * marm)
        If dam <= 0 Then dam = 1
        'Проверим, есть ли опознанное магическое оружие в обоих слотах, .
        spl1 = pchar.GetWeaponSpell(wPrimary)
        spl2 = pchar.GetWeaponSpell(wSecondary)
        'Проверим заклинание «голиаф» (добавляет силу).
        If spl1.id = splGoliath Then dam += pchar.CurrStr
        If spl2.id = splGoliath Then dam += pchar.CurrStr
        'Изменим здоровье монстра.
        isdead = level.ApplyDamage(mx, my, dam)
        'Убедимся, что монстр жив.
        If isdead = FALSE Then
           'Применим заклинание.
           If spl1.id <> splNone Then
              isdead = DoWeaponSpell(spl1, mx, my)
           EndIf
           If isdead = FALSE Then
              If spl2.id <> splNone Then
                 isdead = DoWeaponSpell(spl2, mx, my)
              End If
           EndIf
        EndIf
        'Если монстр умер.
        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

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

dod.bas:DoMeleeCombat

'Проверим, есть ли опознанное магическое оружие в обоих слотах, .
        spl1 = pchar.GetWeaponSpell(wPrimary)
        spl2 = pchar.GetWeaponSpell(wSecondary)
        'Проверим заклинание «голиаф» (добавляет силу).
        If spl1.id = splGoliath Then dam += pchar.CurrStr
        If spl2.id = splGoliath Then dam += pchar.CurrStr
        'Изменим здоровье монстра.
        isdead = level.ApplyDamage(mx, my, dam)
        'Убедимся, что монстр жив.
        If isdead = FALSE Then
           'Применим заклинание.
           If spl1.id <> splNone Then
              isdead = DoWeaponSpell(spl1, mx, my)
           EndIf
           If isdead = FALSE Then
              If spl2.id <> splNone Then
                 isdead = DoWeaponSpell(spl2, mx, my)
              End If
           EndIf
        EndIf

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

character.bi

'Возвращает информацию о заклинании
 Function character.GetWeaponSpell(wslot As wieldpos) As spelltype
   Dim ret As spelltype
   
   'Очистим структуру.
   ClearSpell ret
         
   'Проверим слот на наличие в нем оружия.
   If _cinfo.cwield(wslot).classid = clWeapon Then
     'Проверим, опознано ли оно.
     If _cinfo.cwield(wslot).weapon.eval = TRUE Then
        'Вернем заклинание; возможно splNone.
        ret = _cinfo.cwield(wslot).weapon.spell
     End If
   End If
   
   Return ret
 End Function

Функция GetWeaponSpell является частью объекта персонажа и просто возвращает данные о заклинании оружия в определенном слоте персонажа, если оно было определено. Подпрограмма ClearSpell устанавливает возвращаемое значение в splNone, так что. Если оружие небыло определено, функция вернет splNone в качестве идентификатора заклинания. Что произойдет если оружие не содержит заклинания? Функция вернет splNone, так как мы вызывали функцию ClearSpell во время создания оружия до назначения ему идентификатора заклинания. Это гарантирует, что всегда вернется правильный идентификатор заклинания, какое бы оружие не находилось в руках у персонажа.

dod.bas

'Применяет активное оружейное заклинание.
 Function DoWeaponSpell (spl As spelltype, mx As Integer, my As Integer) As Integer
   Dim As Integer ret = FALSE, statamt, rstat
   Dim As monStats mstat
   Dim As String splname
   
   If spl.id = splThief Then
     'Получим случайную характеристику.
     mstat = RandomRange(CombatFactor, MagicDefense)
     statamt = level.GetMonsterStatAmt(mstat, mx, my)
     rstat = RandomRange(1, statamt - 1)
     'Добавим временное бонусное значение характеристики персонажу.
     Select Case mstat
        Case CombatFactor
           pchar.BonAcf = rstat
           pchar.BonAcfCnt = spl.lvl
        Case CombatDefense
           pchar.BonCdf = rstat
           pchar.BonCdfCnt = spl.lvl
        Case MagicCombat
           pchar.BonMcf = rstat
           pchar.BonMcfCnt = spl.lvl
        Case MagicDefense
           pchar.BonMdf = rstat
           pchar.BonMdfCnt = spl.lvl
     End Select
   Else
     'Применим заклинание к монстру.
     ret = level.ApplySpell(spl, mx, my)
   EndIf
   
   Return ret
      
 End Function

DoWeaponSpell применяет эффект заклинания к монстру. Обратите внимание, что мы проверяем, является ли текущее заклинание. Заклинанием «Вор», так как оно добавляет бонус украденной у монстра характеристики персонажу. Различные боевые характеристики монстра перечислены в файле monster.bi.

monster.bi

'Характеристики монстров, используемых заклинаниями.
 Enum monStats
   CombatFactor = 1
   CombatDefense
   MagicCombat
   MagicDefense
 End Enum

Вначале случайным образом выбирается боевая характеристика монстра, а затем, при помощи функции GetMonsterStatAmt получаем ее значение.

monster.bi

'Возвращает текущее значение характеристики монстра
 Function levelobj.GetMonsterStatAmt(stat As monStats, mx As Integer, my As Integer) As Integer
   Dim As Integer midx, ret = 0
   
   If _level.lmap(mx, my).monidx > 0 Then
     midx = _level.lmap(mx, my).monidx
     If stat = CombatFactor Then
        ret = _level.moninfo(midx).cf
     ElseIf stat = CombatDefense Then
        ret = _level.moninfo(midx).cd
     ElseIf stat = MagicCombat Then
        ret = _level.moninfo(midx).mf
     ElseIf stat = MagicDefense Then
        ret = _level.moninfo(midx).md
     EndIf
  End If
   
  Return ret
End Function

Функция просто проверяет — значение какого параметра нам нужно и возвращает его. Так как список монстров у нас находится в объекте уровня подземелья. То данная функция также принадлежит объекту уровня.

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

dod.bas:DoWeaponSpell

...
     Select Case mstat
        Case CombatFactor
           pchar.BonAcf = rstat
           pchar.BonAcfCnt = spl.lvl
...

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

Если передаваемое в функцию DoWeaponSpell заклинание не является заклинанием «Вор», то мы должны применить его эффект на монстра, что мы и делаем при помощи функции объекта уровня ApplySpell.

map.bi

'Применить заклинание на монстра.
 Function levelobj.ApplySpell(spl As spelltype, mx As Integer, my As Integer) As Integer
   Dim As Integer ret, midx
   Dim stat As monStats
   Dim As String txt
   
   'Убедимся что здесь есть монстр.
   If _level.lmap(mx, my).monidx > 0 Then
     'Получим индекс монстра.
     midx = _level.lmap(mx, my).monidx
     'Установим монстру флаг в зависимости от заклинания.
     Select Case spl.id
        Case splSerpentBite 'Оружие: Наносит урон ядом.
           ret = ApplyDamage(mx, my, spl.dam)
           'Если не умер, установим флаг времени.
           If ret = FALSE Then
              _level.moninfo(midx).effects(mePoison).cnt = spl.lvl
              _level.moninfo(midx).effects(mePoison).dam = spl.dam
           EndIf
           txt = "Sperpent Bite Spell does " & spl.dam & " to " & _level.moninfo(midx).mname & "."
           PrintMessage txt
        Case splRend        'Оружие: Уменьшает броню цели.
           _level.moninfo(midx).armval = _level.moninfo(midx).armval - (spl.dam / maxlevel)
           If _level.moninfo(midx).armval < 0.0 Then
              _level.moninfo(midx).armval = 0.0
           EndIf
           txt = "Rend Spell reduces armor to  " & _level.moninfo(midx).armval & " for " & _level.moninfo(midx).mname & "."
           PrintMessage txt
        Case splSunder      'Оружие: Снижает наносимый целью урон оружием.
           _level.moninfo(midx).atkdam = _level.moninfo(midx).atkdam - spl.dam
           If _level.moninfo(midx).atkdam < 1 Then
              _level.moninfo(midx).atkdam = 1
           EndIf
           txt = "Sunder Spell reduces attack damage to  " & _level.moninfo(midx).atkdam & " for " & _level.moninfo(midx).mname & "."
           PrintMessage txt
        Case splReaper 'Оружие: Вызывает у монстра приступ страха.
           _level.moninfo(midx).flee = TRUE
           txt = "Reaper Spell is making " & _level.moninfo(midx).mname & " flee."
           PrintMessage txt
        Case splFire        'Оружие: Поджигает цель.
           ret = ApplyDamage(mx, my, spl.dam)
           'If not dead set the timed flag.
           If ret = FALSE Then
              _level.moninfo(midx).effects(meFire).cnt = spl.lvl
              _level.moninfo(midx).effects(meFire).dam = spl.dam
           EndIf
           txt = "Fire Spell inflicted  " & spl.dam & " to " & _level.moninfo(midx).mname & "."
           PrintMessage txt
        Case splStun        'Оружие: Обездвиживает цель на время.
           _level.moninfo(midx).effects(meStun).cnt = spl.lvl
           _level.moninfo(midx).effects(meStun).dam = 0
           txt = "Stun Spell stunned " & _level.moninfo(midx).mname & "."
           PrintMessage txt
        Case splChaos       'Оружие: Добавляет случайные повреждения.
           ret = ApplyDamage(mx, my, spl.dam)
           txt = "Chaos Spell inflicted  " & spl.dam & " additional damage to " & _level.moninfo(midx).mname & "."
           PrintMessage txt
        Case splWraith      'Оружие: Уменьшает случайную характеристику цели.
           'Get the stat.
           stat = RandomRange(CombatFactor, MagicDefense)
           Select Case stat
              Case CombatFactor
                 _level.moninfo(midx).cf = _level.moninfo(midx).cf - spl.dam
                 If _level.moninfo(midx).cf < 0 Then
                    _level.moninfo(midx).cf = 1
                 EndIf
                 txt = "Wraith Spell reduced " & _level.moninfo(midx).mname & " combat factor by " & spl.dam & "."
                 PrintMessage txt
              Case CombatDefense
                 _level.moninfo(midx).cd = _level.moninfo(midx).cd - spl.dam
                 If _level.moninfo(midx).cd < 0 Then
                    _level.moninfo(midx).cd = 1
                 EndIf
                 txt = "Wraith Spell reduced " & _level.moninfo(midx).mname & " combat defense by " & spl.dam & "."
                 PrintMessage txt
              Case MagicCombat
                 _level.moninfo(midx).mf = _level.moninfo(midx).mf - spl.dam
                 If _level.moninfo(midx).mf < 0 Then
                    _level.moninfo(midx).mf = 1
                 EndIf
                 txt = "Wraith Spell reduced " & _level.moninfo(midx).mname & " magic combat by " & spl.dam & "."
                 PrintMessage txt
              Case MagicDefense
                 _level.moninfo(midx).md = _level.moninfo(midx).md - spl.dam
                 If _level.moninfo(midx).md < 0 Then
                    _level.moninfo(midx).md = 1
                 EndIf
                 txt = "Wraith Spell reduced " & _level.moninfo(midx).mname & " magic defense by " & spl.dam & "."
                 PrintMessage txt
           End Select
     End Select
   End If
   
   Return ret
 End Function

Первое что нужно сделать, это убедиться. Что монстр, на которого нужно применить заклинание, присутствует. Если это так, то мы применяем на наго эффект заклинания основываясь на его идентификаторе. Каждое заклинание делает что то свое и влияет на монстра по разному. Обратите внимание, что здесь мы имеем дело именно с самим заклинанием, а не его источником. Это позволит добавлять в эту функцию любые заклинания применимые к монстрам, а не только заклинания привязанные к оружию. Именно для этого нам нужно было обобщить заклинания, в результате чего мы будем использовать набор одних и тех же процедур для управления всеми заклинаниями в игре.

Изучив код вы можете увидеть что конкретно делает каждое заклинание. Обратите внимание, что поля lvl и dam в каждом заклинании используются по разному. Поле dam является универсальным и используется не только для нанесения повреждений, но, также, и для снижения атрибутов монстра. Некоторые заклинания влияют на монстров с течением времени. Например, заклинание «Жнец», заставляет монстра убегать, установив в TRUE специальный атрибут. Заклинание «Оглушить» временно замораживает монстра, заставляя его пропуска все атаки и передвижения. Продолжительность большинства эффектов зависит от уровня заклинания, поэтому чем выше уровень заклинания, тем дольше его эффект будет действовать на монстра.

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

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
   Dim As spelltype spl1, spl2
   
   'Получим фактор защиты монстра.
   If pchar.ProjectileIsWand(wslot) = TRUE Then
     df = level.GetMonsterMagicDefense(mx, my)
   Else
     df = level.GetMonsterDefense(mx, my)
   EndIf
   'Получим имя монстра.
   mname = level.GetMonsterName(mx, my)
   'Получим здоровье монстра.
   mxp = level.GetMonsterXP(mx, my)
   'Убедимся что данные получены.
   If df > 0 Then
     'Получим файтор атаки, основанный на том, чем вооружен персонаж.
     If pchar.ProjectileIsWand(wslot) = TRUE Then
        cf = pchar.GetMagicCombatFactor()
     Else
        cf = pchar.GetProjectileCombatFactor()
     EndIf
     'Возьмем случайные значения.
     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
        'Проверим заклинания для оружия в обоих слотах. Если опознано.
        spl1 = pchar.GetWeaponSpell(wPrimary)
        spl2 = pchar.GetWeaponSpell(wSecondary)
        'Если есть заклинание «голиаф».
        If spl1.id = splGoliath Then dam += pchar.CurrStr
        If spl2.id = splGoliath Then dam += pchar.CurrStr
        'Изменим здоровье монстра.
        isdead = level.ApplyDamage(mx, my, dam)
        'Если монстр все еще жив.
        If isdead = FALSE Then
           'Применим эффект заклинания.
           If spl1.id <> splNone Then
              isdead = DoWeaponSpell(spl1, mx, my)
           EndIf
           If isdead = FALSE Then
              If spl2.id <> splNone Then
                 isdead = DoWeaponSpell(spl2, mx, my)
              End If
           EndIf
        EndIf
        'Если монстр умер.
        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


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

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