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

mcombat0.png

Сейчас мы достигли точки, где уже возможно участвовать в ближнем бою. Чтобы атаковать, мы просто врезаться в монстра, в результате чего, нападаем на него с тем, что у нас есть под рукой в данный момент. Реализовано все это в подпрограмме 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


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

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

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

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