Давайте сделаем рогалик (Магия оружия)

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

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

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

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

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