четверг, 15 марта 2012 г.

Давайте сделаем рогалик. Глава 19: Ближний бой

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

dod.bas
'Передвижение персонажа, основанное на направлении по компасу.
  Function MoveChar(comp As compass) As Integer
    Dim As Integer ret = FALSE, block
    Dim As vec vc = vec(pchar.Locx, pchar.Locy) 'Creates a vector object.
    Dim As terrainids tileid
    Dim As Integer snd
  
    vc+= comp
    'Убедимся, что не вышли за границы карты.
    If (vc.vx >= 1) And (vc.vx <= mapw) Then
      If (vc.vy >= 1) And (vc.vy <= maph) Then
         'Проверим, заблокирована ли ячейка карты.
         block = level.IsBlocking(vc.vx, vc.vy)
         'Проверим, есть ли на ячейке монстр.
         If block = FALSE Then
            block = level.IsMonster(vc.vx, vc.vy)
            'Проверим столкновение с монстром.
            If block = TRUE Then
               'Атакуем монстра.
               DoMeleeCombat vc.vx, vc.vy
               ret = TRUE
            EndIf
         EndIf

Функция IsMonster является частью объекта уровня и возвращает TRUE если по координатам x, y карты уровня находится монстр.

map.bi
'Возвращает «истина» если по указанным координатам находится монстр.
  Function levelobj.IsMonster(x As Integer, y As Integer) As Integer
    Dim As Integer ret = FALSE
  
    If _level.lmap(x, y).monidx > 0 Then
      ret = TRUE
    EndIf
  
    Return ret
  End Function

Если возвращается значение TRUE, то для боя с монстром мы вызываем подпрограмму DoMeleeeCombat.

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
  
    'Получим фактор защиты монстра.
    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
         'Изменим здоровье монстра.
         isdead = level.ApplyDamage(mx, my, dam)
         'Напечатаем сообщение.
         If isdead = TRUE Then
            'Добавим персонажу опыт.
            xp = pchar.CurrXP
            xp += mxp
            pchar.CurrXP = xp
            xp = pchar.TotXP
            xp += mxp
            pchar.TotXP = xp
            'Печатаем сообщение.
            txt = pchar.CharName & " killed the " & mname & " with " & dam & " damage points."
            PrintMessage txt
         Else
            txt = pchar.CharName & " hit the " & mname & " for " & dam & " damage points."
            PrintMessage txt
         EndIf
      Else
         txt = pchar.CharName & " missed the " & mname & "."
         PrintMessage txt
      EndIf
    EndIf
  End Sub

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

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

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

Первое что мы должны сделать, это получить фактор защиты монстра.

map.bi
'Возвращает фактор зашиты монстра.
  Function levelobj.GetMonsterDefense(mx As Integer, my As Integer) As Integer
    Dim As Integer ret = 0, midx = 0
  
    'Убедимся что монстр есть в этих координатах.
    If _level.lmap(mx, my).monidx > 0 Then
      midx = _level.lmap(mx, my).monidx
      ret = _level.moninfo(midx).cd
    EndIf
  
    Return ret
  End Function

В функцию передаются x и y координаты в которых на карте находится монстр. Используя их мы получаем id монстра из массива монстров на карте и уже из него получаем значение возвращаемого функцией параметра cd у конкретного монстра.

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

map.bi
'Возвращает имя монстра
  Function levelobj.GetMonsterName(mx As Integer, my As Integer) As String
    Dim As String ret
    Dim As Integer midx
  
    'Убедимся что монстр есть в этих координатах.
    If _level.lmap(mx, my).monidx > 0 Then
      midx = _level.lmap(mx, my).monidx
      ret = _level.moninfo(midx).mname
    EndIf
  
    Return ret
  End Function

Эта функция аналогичны предыдущей, только возвращает не фактор защиты монстра а значение параметра mname в котором хранится его имя.

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

map.bi
'Возвращает кол-во опыта, начисляемого за убийство монстра.
  Function levelobj.GetMonsterXP(mx As Integer, my As Integer) As Integer
    Dim As Integer midx, ret
  
    'Убедимся что монстр присутствует.
    If _level.lmap(mx, my).monidx > 0 Then
      midx = _level.lmap(mx, my).monidx
      ret = _level.moninfo(midx).xp
    EndIf
  
    Return ret
  End Function

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

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

character.bi
'Возвращает текущтй боевой фактор персонажа основываясь на его вооружении.
  Function character.GetMeleeCombatFactor() As Integer
    Dim As Integer ret
  
    'Безоружный бой.
    If (_cinfo.cwield(wPrimary).classid <> clWeapon) And (_cinfo.cwield(wSecondary).classid <> clWeapon) Then
      ret = CurrUcf + BonUcf
    Else
      'Проверим главный оружейный слот.
      If _cinfo.cwield(wPrimary).classid = clWeapon Then
         'Орудие ближнего боя.
         If _cinfo.cwield(wPrimary).weapon.id < wpSling Then
            ret = CurrAcf + BonAcf
         Else
          'Возвращаем фактор безоружного боя если в руках нет контактного оружия.
           ret = CurrUcf + BonUcf
         EndIf
      Else
         If _cinfo.cwield(wSecondary).classid = clWeapon Then
            'Орудие ближнего боя.
            If _cinfo.cwield(wSecondary).weapon.id < wpSling Then
               ret = CurrAcf + BonAcf
            Else
               'Возвращаем фактор безоружного боя если в руках нет контактного оружия.
               ret = CurrUcf + BonUcf
            End If
         EndIf
      End If
    EndIf
  
    Return ret     
  End Function

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

Если персонаж попал по монстру, то мы должны получить вулицину урона, наносимого используемым оружием.

character.bi
'Возвращает величину урона наносимого оружием.
  Function character.GetWeaponDamage() As Integer
    Dim As Integer ret = 0
  
    'Посмотрим, есть ли оружие.
   If (_cinfo.cwield(wPrimary).classid <> clWeapon) And (_cinfo.cwield(wSecondary).classid <> clWeapon) Then
      'Если оружия нет, то повреждения зависят от силы и бонуса силы.
      ret = (_cinfo.stratt(0) + _cinfo.stratt(1)) / 2
    Else
      'Есть одно или более одного оружия.
      If _cinfo.cwield(wPrimary).classid = clWeapon Then
         'Получим повреждения от текущего оружия.
         ret = _cinfo.cwield(wPrimary).weapon.dam
      EndIf
      If _cinfo.cwield(wSecondary).classid = clWeapon Then
         ret += _cinfo.cwield(wSecondary).weapon.dam
      EndIf
    EndIf
  
    Return ret
  End Function

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

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

map.bi
'Возвращает рейтинг брони монстра.
  Function levelobj.GetMonsterArmor(mx As Integer, my As Integer) As Single
    Dim As Single ret
    Dim As Integer midx
   
    'Убедимся что монстр есть по указанным координатам.
    If _level.lmap(mx, my).monidx > 0 Then
      midx = _level.lmap(mx, my).monidx
      ret = _level.moninfo(midx).armval
    EndIf
  
    Return ret
  End Function

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

dod.bas
'Получим фактор брони монстра.
         marm = level.GetMonsterArmor(mx, my)
         'Рассчитаем нанесенные повреждения.
         dam = dam - (dam * marm)
         If dam <= 0 Then dam = 1
         'Изменим здоровье монстра.
         isdead = level.ApplyDamage(mx, my, dam)

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

map.bi
'Наносит повреждения монстру. Возвращает «истина» если монстр умер.
  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
  
    'Проверим что монстр находится здесь.
    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 <= 0 Then
         'Монстр мертв.
         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
    EndIf
  
    Return ret
  End Function

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

Конечно, не только игрок может атаковать монстров, но и монстры игрока. Монстры получают свою очередь для атаки в цикле их передвижения.

map.bi:MoveMonsters
...
  'Проверим расстояние до персонажа.
  pdist = CalcDist(_level.moninfo(i).currcoord.x, pchar.Locx, _level.moninfo(i).currcoord.y, pchar.Locy)
  'Проверим дальность атаки монстра.
  If pdist <= _level.moninfo(i).atkrange Then
    'Атакуем персонажа.
    MonsterAttack _level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y
  Else
...

Мы уже рассматривали эту процедуру в предыдущей главе, сейчас мы просто добавили вызов подпрограммы MonsterAttack.

map.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 Then
      midx = _level.lmap(mx, my).monidx
      'Получим фактор защиты персонажа.
      cd = pchar.GetDefenseFactor()
      'Получим фактор атаки монстра.
      mc = _level.moninfo(midx).cf
      'Получим случайные значения.
      rollc = RandomRange(1, cd)
      rollm = RandomRange(1, mc)
      'Сравним значения монстра и персонажа.
      If rollm > rollc Then
         'Получим величину повреждений.
         dam = _level.moninfo(midx).atkdam
         'Получим броню щитов.
         arm = pchar.GetShieldArmorValue()
         'Поглотим часть повреждений щитами.
         If arm > 0 Then
            dam = dam - (dam * arm)
         End If
         'Получим броню доспехов.
         arm = pchar.GetArmorValue ()
         'Поглотим часть повреждений доспехами.
         If arm > 0 Then
            dam = dam - (dam * arm)
         End If
         If dam < 1 Then dam = 1
         'Получим здоровье персонажа.
         chp = pchar.CurrHP
         'Вычтем повреждения.
         chp -= dam
         If chp < 0 Then chp = 0
         'Обновим здоровье персонажа.
         pchar.CurrHP = chp
         txt = "The " &  _level.moninfo(midx).mname & " hits for " & dam & " damage points."
      Else
         txt = "The " &  _level.moninfo(midx).mname & " misses."
      EndIf
      PrintMessage txt
    End If   
  End Sub

Подпрограмма MonsterAttack практически идентична DoMeleeCombat. Мы получаем значения атакующего боевого фактора монстра и защитного фактора персонажа. На основе их генерируем случайные числа и сравниваем, что бы определить — попал монстр по персонажу или нет. Если монстр попал, то необходимо получить значение брони персонажа и шита (если есть), причем расчет поглощаемых повреждений происходит в 2 этапа. Вначале повреждения поглощаются значением брони щита, так как это первая лини обороны, оставшиеся повреждения участвуют в расчете поглощаемых повреждений доспехами. Так же как и для повреждений монстра, повреждения персонажа должны быть минимум 1 единица здоровья.

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

character.bi
'Возвращает фактор зашиты персонажа.
  Function character.GetDefenseFactor () As Integer
    Return currCdf + BonCdf
  End Function

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

character.bi
'Возвращает текущее значение брони персонажа.
  Function character.GetArmorValue() As Single
    Dim As Single ret = 0.0
  
    'Проверим, использует ли персонаж броню.
    If _cinfo.cwield(wArmor).classid <> clNone Then
      ret = _cinfo.cwield(wArmor).armor.dampct 
    EndIf
  
    Return ret
  End Function

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

character.bi
'Возвращает значение брони щита, используемого персонажем.
  Function character.GetShieldArmorValue () As Single
    Dim As Single ret = 0.0
    Dim As Integer cnt
  
    'Проверим возможные слоты расположения щита.
    If _cinfo.cwield(wPrimary).classid = clShield Then
      ret += _cinfo.cwield(wPrimary).shield.dampct
      cnt = 1
    EndIf   
    If _cinfo.cwield(wSecondary).classid = clShield Then
      ret += _cinfo.cwield(wSecondary).shield.dampct
      cnt += 1
    EndIf
  
    'Возьмем среднее значение если используется более одного.
    If cnt > 0 Then
      ret = ret / cnt
    EndIf
  
    Return ret
  End Function

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

Проверка на смерть персонажа добавлена в основной цикл программы.

dod.bas
'Так как надата клавиша обработаем необходимое действие.
         pchar.DoTimedActions
         'Проверим, умер ли персонаж?.
         If pchar.CurrHP <= 0 Then
            isdead = TRUE
         EndIf

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

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

1 комментарий: