Давайте сделаем рогалик (Дистанционный бой)
При первом рассмотрении, кажется, что добавить дистанционные атаки достаточно просто. Мы должны выбрать цель, запустить снаряд и проверить — попали ли мы им куда либо. Рассмотрев задачу более подробно, мы можем разложить ее на несколько простых подзадач, которые нам необходимо реализовать:
- Система выбора цели
- Боеприпасы и их количество
- Система перезарядки оружия
- Анимация атаки
- Расчет повреждений
Как вы можете видеть, действий, которые необходимо совершить, достаточно много, и их реализация потребует достаточного количества кода. Рассмотрим все по порядку.
Как обычно, мы начнем с добавления определения новых типов данных. Вы уже видели определение типа данных для оружия, но нам необходимо его изменить, для добавления такого параметра как боеприпасы. Это подводит нас к определению типа данных боеприпасов, которые будут находится в инвентаре персонажа и будут использоваться оружием по необходимости. Итак, давайте начнем с обновления типа данных для оружия.
inv.bi
'Оружие. Type weapontype id As weaponids 'Тип орежия evaldr As Integer 'Сложность опознания. Если > 0 то магический предмет. eval As Integer 'Если опознано. spell As spellid 'Заклинание. 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 'Кол-во снарядов в обойме. End Type
Перечисление Weaponids позволит легко определить — является оружие
контактным или дистанционным.
inv.bi
'Типы оружия. Enum weaptype wtNone wtMelee wtProjectile End Enum
С помощью перечисления мы несколько упростим код идентификации, как вы
можете убедиться немного позже.
Следующее новое поле в описании типа оружия, это емкость обоймы (в нашем случае - ствол арбалета). Это позволит нам создавать оружие, не требующее перезарядки после каждого выстрела. Большая часть оружия у нас будет однозарядными, так как мы создаем фентезийную игру использующую средневековый стиль оружия. Тем не менее, мы можем использовать эту же структуру и для фантастической игры, где луч лазера может выстрелить раз сто без перезарядки. Удобство данного подхода в том, что данная система также прекрасно работает и для однозарядного оружия, в результате мы получаем большую гибкость кода.
Тип боеприпасов задается перечислением ammoids.
inv.bi
'Типы боеприпасов. Enum ammoids amNone amBagStones amCaseBolts amQuiverArrows End Enum
Здесь у нас задаются три типа боеприпасов: камни для пращи, болты для
арбалетов и стрелы для луков. Каждый из них будет содержать количество зарядов.
Обратите внимание, что у нас нет боеприпасов по отдельности, т. е. одна стрела
или один камень. Я выбрал такой дизайн из за моей нелюбви к боеприпасам по
одиночке. Очевидно, что одна стрела, сама по себе, бесполезна, потому я заменю
их на «коробки с патронами». Однако это добавит в игру спорный фактор — после
выстрела снаряд исчезнет, так как у нас нет снарядов по одиночке. Хотя это может
показаться странным, но есть разумное обоснование для этого — боеприпасы
становятся бесполезны после их использования. Стрелы и камни, попадая в броню,
вероятнее всего разрушились бы в «реальной жизни». Хотя, на самом деле, это
всего лишь упрощение и мои собственные предпочтения о вопросе с зарядами.
Последнее поле в описании типа оружия, это ammocnt , которое отвечает за количество снарядов, заряженных в оружие. В большинстве случаев оно будет равно 0 или 1, но в случае с арбалетом, у которого емкость обоймы 6, данный параметр будет варьироваться от 0 до 6.
Когда мы создаем новое оружие нам нужно заполнить эти поля.
inv.bi:GenerateWeapon
Case wpBarrelcrossbow 'двуручное, повреждения 18 inv.desc = "Barrel Crossbow" inv.icon = Chr(209) inv.weapon.noise = 12 inv.weapon.dam = 18 inv.weapon.hands = 2 inv.weapon.weapontype = wtProjectile inv.weapon.capacity = 6 inv.weapon.ammotype = amCaseBolts
Это описание для нашего 6-ти зарядного арбалета. Обратите внимание, что
емкость обоймы устанавливается равной 6-ти, что позволит нам заряжать в этот
арбалет до 6-ти болтов. Тип боеприпасов задан как amCaseBolts, что означает, что
данное оружие использует болты а не стрелы или камни.
Теперь, когда мы разобрались с оружие, давайте рассмотрим описание типа для боеприпасов.
inv.bi
'Боеприпасы. Type ammotype id As ammoids cnt As Integer noise As Integer eval As Integer End Type
Здесь все просто и должно быть понятно. Единственная причина, из за
которой мы добавили отдельный тип предмета для зарядов, это возможность хранения
персонажем в инвентаре снарядов различных типов для разных типов оружия.
Ключевое поле здесь, это col — которое показывает количество боеприпасов
содержащихся в данной структуре. Персонажу нет необходимости определять снаряды,
так как у них нет каких либо специальных свойств, но мы добавили поле eval,
которое необходимо для правильной работы процедуры опознания предметов. Когда мы
будем создавать боеприпасы, мы будем отмечать их как уже опознанные, для
правильного отображения в инвентаре.
inv.bi
'Создает боеприпасы. Sub GenerateAmmo(inv As invtype, currlevel As Integer, ammoid As ammoids = amNone) Dim item As ammoids If ammoid = amNone Then item = RandomRange(amBagStones, amQuiverArrows) Else item = ammoid End If 'Установим идентификатор типа. inv.classid = clAmmo inv.ammo.id = item inv.iconclr = fbCadmiumYellow inv.ammo.eval = TRUE Select Case item Case amBagStones inv.desc = "Bag of Stones" inv.ammo.cnt = RandomRange(10, 20) inv.ammo.noise = inv.ammo.cnt inv.icon = Chr(167) Case amCaseBolts inv.desc = "Case of Bolts" inv.ammo.cnt = RandomRange(10, 20) inv.ammo.noise = inv.ammo.cnt inv.icon = Chr(22) Case amQuiverArrows inv.desc = "Quiver of Arrows" inv.ammo.cnt = RandomRange(10, 20) inv.ammo.noise = inv.ammo.cnt inv.icon = Chr(173) Case Else inv.desc = "Unknown Ammo" inv.ammo.cnt = 0 inv.ammo.noise = 0 inv.icon = "?" End Select End Sub
Как видите, тут используется наш стандартный формат процедуры генерации
предметов. Все эти подпрограммы идентичны и легко узнаваемы, и, что более важно,
это позволяет сразу увидеть что все параметры заданы верно. В результате это
поможет избавится от ошибок, так как каждый элемент создается идентичным
образом. Используя это идею «шаблонов» в своих программах, вы сделаете код легче
читаемым и более простым в отладке и поддержке.
Так как каждый элемент содержит поле количества боеприпасов, мы задаем его для всех предметов случайным числом от 10 до 20. Это поле является ключевым и мы будем его использовать для перезарядки нашего оружия.
Дистанционное оружие бесполезно, если в нем нет боеприпасов, поэтому мы должны реализовать команду перезарядки оружия. Перезаряжаем его мы в процедуре LoadAmmo.
dod.bas
'Перезарядка дистанционного оружия. Sub LoadAmmo () Dim amid As ammoids Dim As Integer ret 'Убедимся, что в руках дистанционное оружие. If pchar.ProjectileEquipped(wAny) = TRUE Then 'Проверим первый слот. If pchar.ProjectileEquipped(wPrimary) = TRUE Then If pchar.IsLoaded(wPrimary) = FALSE Then 'Получим тип боеприпасов. amid = pchar.GetAmmoID(wPrimary) 'Проверим на правильность типа. If amid <> amNone Then 'Проверим инвентарь на наличие боеприпасов. ret = pchar.LoadProjectile(amid, wPrimary) If ret = TRUE Then PrintMessage "Weapon is loaded." Else PrintMessage "Weapon could not be loaded." Endif Endif Else PrintMessage "Weapon is already loaded." Endif End If If pchar.ProjectileEquipped(wSecondary) = TRUE Then If pchar.IsLoaded(wSecondary) = FALSE Then 'Check the inventoru and load weapon if ammo is present. ret = pchar.LoadProjectile(amid, wSecondary) If ret = TRUE Then PrintMessage "Weapon is loaded." Else PrintMessage "Weapon could not be loaded." Endif Else PrintMessage "Weapon is already loaded." Endif Endif Else PrintMessage "Not carrying a projectile weapon." Endif End Sub
Нам необходимы две проверки — на наличие оружия в основном оружейном
слоте, и дополнительном. Большинство дистанционного оружия используют оба
оружейных слота, за исключением пращи, которая может быть использована одной
рукой. Если бы у нас не было одноручного дистанционного оружия, то нам не
понадобилась бы дополнительная проверка вторичного оружейного слота. Так как оба
кода идентичны, то рассмотрим проверку только основного слота.
Первое, что нужно сделать, это убедится, что у персонаджа в руках дистанционное оружие, это делает метод ProjectileEquipped объекта персонажа.
character.bi
'Возвращает True если персонаж вооружен дистанционным оружием. Function character.ProjectileEquipped(slot As wieldpos) As Integer Dim As Integer ret = FALSE 'Убедимся что classid это оружие. If _cinfo.cwield(wPrimary).classid = clWeapon Then 'Убедимся что оружие дистанционное. If _cinfo.cwield(wPrimary).weapon.weapontype = wtProjectile Then 'Проверим слот. If (slot = wAny) Or (slot = wPrimary) Then ret = TRUE End If Endif Else 'Убедимся что classid это оружие. If _cinfo.cwield(wSecondary).classid = clWeapon Then 'Убедимся что оружие дистанционное. If _cinfo.cwield(wSecondary).weapon.weapontype = wtProjectile Then 'Проверим слот. If (slot = wAny) Or (slot = wSecondary) Then ret = TRUE End If Endif Endif Endif Return ret End Function
Вначале необходимо убедиться, что персонаж держит в руках оружие (в
первичном или вторичном слотах), затем необходимо проверить тип оружия, для того
чтобы убедиться что оружие дистанционное. Если оно дистанционное, то мы
возвращает true. Обратите внимание, что для указания слооты мы используем не
только типы wPrimary и wSecondary, но и тип wAny — указывающий на то, что оружие
может быть в любой руке. Тип проверяемого слота передается в функцию и если он
будет иметь значение wAny и в первичном слоте обнаружиться дистанционное оружие,
то мы выйдем из функции без проверки вторичного слота.
В LoadAmmo после подтверждения нахождения в руках персонажа дистанционного оружия мы переходим к рассмотрению каждого из оружейных слотов по отдельности.
dod.bas:LoadAmmo
... If pchar.ProjectileEquipped(wPrimary) = TRUE Then If pchar.IsLoaded(wPrimary) = FALSE Then ...
Следующее что нам нужно сделать. это проверить — заряжено ли оружие. Если
оно уже заряжено, то перезаряжать его не имеет смысла. Функция IsLoaded
сообщает, заряжено ли оружие полностью или нет.
character.bi
'Возвращает true если дистанционное орудие в руках заряжено. Function character.IsLoaded(slot As wieldpos) As Integer Dim As Integer ret = FALSE 'Убедимся что в руках оружие. If _cinfo.cwield(slot).classid = clWeapon Then 'Убедимся что оно дистанционное. If _cinfo.cwield(slot).weapon.weapontype = wtProjectile Then 'Проверим на кол-во боеприпасов. ret = (_cinfo.cwield(slot).weapon.ammocnt < _cinfo.cwield(slot).weapon.capacity) Endif Endif Return ret End Function
Тут мы просто сравниваем значение параметра оружия ammocnt (заряженных
снарядов) и capacity (емкость обоймы). Если у нас оружие многозарядное, то мы
можем его перезарядить в любой удобный момент, а не только тогда, когда обойма
пуста.
После того, как мы определили что оружие может быть перезаряжено мы должны выяснить, какие боеприпасы ему необходимы.
dod.bas:LoadAmmo
... 'Получим идентификатор боеприпасов. amid = pchar.GetAmmoID(wPrimary) 'Проверим на правильность идентификатора. If amid <> amNone Then ...
Функция GetAmmoId возвращает идентификатор боеприпасов для выбранного
оружия.
character.bi
'Возвращает идентификатор боеприпасов для дистанционного оружия в выбранном слоте. Function character.GetAmmoID(slot As wieldpos) As ammoids Dim ret As armorids 'Убедимся что в слоте оружие. If _cinfo.cwield(slot).classid = clWeapon Then 'Убедимся что оно дистанционное. If _cinfo.cwield(slot).weapon.weapontype = wtProjectile Then ret = _cinfo.cwield(slot).weapon.ammotype Else ret = amNone Endif Else ret = amNone Endif Return ret End Function
Мы проверяем указанный слот и возвращает идентификатор типа боеприпасов
для используемого оружия, который мы задавали при его создании.
dod.bas:LoadAmmo
... 'Проверим инвентарь и перезарядим оружие если есть боеприпасы. ret = pchar.LoadProjectile(amid, wPrimary) If ret = TRUE Then PrintMessage "Weapon is loaded." Else PrintMessage "Weapon could not be loaded." Endif ...
После получения идентификатора необходимых боеприпасов, мы можем
перезарядить оружие. Это реализовано в методе LoadProjectile объекта
персонажа.
character.bi
'Заряжает оружие и возвращает True в случае успеха. Function character.LoadProjectile(aid As ammoids, slot As wieldpos) As Integer Dim As Integer ret = FALSE, i, iitem Dim As invtype inv 'Провеним инвентарь на наличие необходимых боеприпасов. For i = LowInv To HighInv 'Выходим, если оружие полностью заряжено. If _cinfo.cwield(slot).weapon.ammocnt = _cinfo.cwield(slot).weapon.capacity Then Exit For Endif iitem = HasInvItem(i) If iitem = TRUE Then 'Получим предмет инвентаря. GetInventoryItem i, inv 'Проверим идентификатор предмета. If inv.classid = clAmmo Then 'Проверим тип боеприпасов. If inv.ammo.id = aid Then 'Зарядим оружие до емкости обоймы. Do While _cinfo.cwield(slot).weapon.ammocnt < _cinfo.cwield(slot).weapon.capacity 'Зарядим оружие. _cinfo.cwield(slot).weapon.ammocnt += 1 inv.ammo.cnt -= 1 'Если закончились боеприпасы — выход из цикла. If inv.ammo.cnt = 0 Then Exit Do Endif Loop 'Обновим инвентарь. If inv.ammo.cnt > 0 Then AddInvItem i,inv Else ClearInv inv AddInvItem i,inv Endif 'Если есть заряженные боеприпасы, то вернем true If _cinfo.cwield(slot).weapon.ammocnt > 0 Then ret = TRUE Endif Endif Endif Endif Next Return ret End Function
В этой функции мы перебираем все предметы инвентаря, используя свойства
LovInv и HighInv, для поиска боеприпасов соответствующих используемому типу
оружия. При нахождении мы запускаем цикл, в котором заряжаем оружие по одному
заряду. Мы выходим из цикла, как только количество находящихся в обойме снарядов
равно емкости обоймы, или закончились боеприпасы в инвентаре. После того как
оружие перезаряжено, мы проверяем количество зарядов в объекте инвентаря, и если
оно равно 0, то удаляем этот объект, иначе возвращаем его в инвентарь.
После того, как оружие заряжено, мы можем его использовать. Первое что нужно сделать - выбрать цель. Мы делаем это в подпрограмме TargetEnemy.
dod.bas
'Выбор цели и атака дистанционным оружием. Sub TargetEnemy () Dim As vec target, source Dim As Integer ret 'Проверим, вооружен ли персонаж дистанционным оружием. If pchar.ProjectileEquipped(wAny) = TRUE Then 'Получим вектор цели. ret = GetTargetCoord(target) source.vx = pchar.Locx source.vy = pchar.Locy 'Игрок нажал enter. If ret = TRUE Then 'Проверим первый слот на дистанционное оружие. If pchar.ProjectileEquipped(wPrimary) = TRUE Then 'Убедимся что заряжено. If pchar.IsLoaded(wPrimary) = TRUE Then 'Выстрелим. level.AnimateProjectile source, target 'Удалим снаряд из оружия. pchar.DecrementAmmo wPrimary 'Убелимся что попали в монстра. If level.IsMonster(target.vx, target.vy) = TRUE Then 'Нанесем повреждения. DoProjectileCombat target.vx, target.vy, wPrimary Else PrintMessage "No monster at target location." End If Else PrintMessage "Weapon is not loaded." Endif Else 'Возможно в другом слоте?. 'Проверим, заряжено ли. If pchar.IsLoaded(wSecondary) = FALSE Then 'Выстрелим. level.AnimateProjectile source, target 'Удалим снаряд из оружия. pchar.DecrementAmmo wSecondary 'Убедимся что попали в монстра. If level.IsMonster(target.vx, target.vy) = TRUE Then 'Нанесем повреждения. DoProjectileCombat target.vx, target.vy, wPrimary Else PrintMessage "No monster at target location." End If Else PrintMessage "Weapon is not loaded." Endif Endif Endif Else PrintMessage "Not carrying a projectile weapon." Endif End Sub
Вначале мы убеждаемся что в руках персонажа дистанционное оружие и
получаем координаты цели при помощи функции GetTargetCoord.
dod.bas
'Возвращает true если персонаж выбрал врага, false — если нет. Координаты сохраняются в векторе vt. Function GetTargetCoord(vt As vec) As Integer Dim As String ch, rtl = "*" Dim As vec v, vtmp Dim As Integer ret = FALSE 'Зададим начальные координаты. v.vx = pchar.Locx v.vy = pchar.Locy 'Зададим новую цель. level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright) level.DrawMap Do ch = Inkey If ch <> "" Then If ch = key_up Then 'Зададим текущие координаты. vtmp = v 'Обновим вектор. vtmp += north 'Проверим ячейку карты. If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then 'Очистим предыдущую цель. level.SetTarget(v.vx, v.vy, 0) 'Сохраним текущие координаты. v = vtmp 'Установим новую цель. level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright) 'Перерисуем карту. Screenlock level.DrawMap Screenunlock End If Endif If ch = key_dn Then 'Зададим текущие координаты. vtmp = v 'Обновим вектор. vtmp += south 'Проверим ячейку карты. If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then 'Очистим предыдущую цель. level.SetTarget(v.vx, v.vy, 0) 'Сохраним текущие координаты. v = vtmp 'Установим новую цель. level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright) 'Перерисуем карту. Screenlock level.DrawMap Screenunlock End If Endif If ch = key_rt Then 'Зададим текущие координаты. vtmp = v 'Обновим вектор. vtmp += east 'Проверим ячейку карты. If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then 'Очистим предыдущую цель. level.SetTarget(v.vx, v.vy, 0) 'Сохраним текущие координаты. v = vtmp 'Установим новую цель. level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright) 'Перерисуем карту. Screenlock level.DrawMap Screenunlock End If Endif If ch = key_lt Then 'Зададим текущие координаты. vtmp = v 'Обновим вектор. vtmp += west 'Проверим ячейку карты. If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then 'Очистим предыдущую цель. level.SetTarget(v.vx, v.vy, 0) 'Сохраним текущие координаты. v = vtmp 'Установим новую цель. level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright) 'Перерисуем карту. Screenlock level.DrawMap Screenunlock End If Endif Endif Sleep 1 Loop Until (ch = key_enter) Or (ch = key_esc) If ch = key_enter Then ret = TRUE Endif 'Очистим предыдущую цель. level.SetTarget(v.vx, v.vy, 0) level.DrawMap vt = v Return ret End Function
В данной функции мы в цикле опрашиваем нажатые пользователем клавиши и в
соответствии с ними перемещаем курсор. Текущие координаты курсора хранятся в
переменной векторного типа vt, значение которой вернется в основную программу
после завершения выбора цели. Так как игрок не может выбрать цель за пределами
видимости персонажа, то мы, вначале, проверяем выбранную ячейку на видимость при
помощи функции IsTileVisible. При нажатии клавиши направления мы обновляем
позицию курсора путем вызова метода SetTarget объекта уровня, а затем вызываем
метод DrawMap для перерисовки карты и отображения курсора на новом месте.
map.bi
'Установить прицел в координаты x, y карты уровня. Sub levelobj.SetTarget(x As Integer, y As Integer, id As Integer, tcolor As Uinteger = fbBlack) _level.lmap(x, y).target.id = id _level.lmap(x, y).target.tcolor = tcolor End Sub
SetTarget обновляет новую структуру данных target, содержащуюся в
описании ячейки карты объекта уровня.
map.bi
'Прицел. Type targettype id As Integer 'Код иконки. tcolor As Uinteger 'Цвет иконки. End Type 'Информация о ячейке карты Type mapinfotype terrid As terrainids 'Тип местности. monidx As Integer 'Индекс монстра в массиве монстров. visible As Integer 'Персонаж видит ячейку. seen As Integer 'Персонаж уже видел ячейку. doorinfo As doortype 'Информация о двери. sndvol As Integer 'Громкость звука в ячейке. target As targettype 'Цель и карта снарядов. End Type
Структура targettype содержит поля для ASCII символа курсора для указания
цели и его цвета. Эта запись находится в структуре mapinfotype, для того, чтобы
мы могли его отобразить в каждой ячейке карты уровня.
Давайте подробнее рассмотрим процесс из GetTargetCoord в одном из направлений движения. Все остальные направления ведут себя таким же образом, за исключением самого направления.
dod.bas:GetTargetCoord
If ch = key_up Then 'Установим текущий вектор. vtmp = v 'Обновим вектор. vtmp += north 'Проверим ячейку карты. If level.IsTileVisible(vtmp.vx, vtmp.vy) = TRUE Then 'Очистим предыдущую цель. level.SetTarget(v.vx, v.vy, 0) 'Сохранить текущую позицию. v = vtmp 'Устновим новую цель. level.SetTarget(v.vx, v.vy, Asc(rtl), fbYellowBright) 'Перерисуем карту. Screenlock level.DrawMap Screenunlock End If Endif
Здесь обрабатывается случай, когда игрок нажал клавишу вверх. Мы
обновляем вектор направления, добавим к текущему значению направление на север,
в результате чего он станет указывать на одну ячейку выше текущих координат.
Затем мы проверяем — видима ли ячейка карты в полученных координатах. Если это
так, то мы очищаем значение курсора передавая 0 в SetTarget, сохраняем
полученные координаты в качестве текущих (для использования во время обработки
следующей клавиши), обновляем массив цели для отображения курсора в новых
координатах, и, наконец, перерисовываем карту уровня. Это обеспечит
реанимированное передвижение курсора по видимым ячейкам карты уровня.
dod.bas:GetTargetCoord
Loop Until (ch = key_enter) Or (ch = key_esc) If ch = key_enter Then ret = TRUE Endif
Мы выходим из цикла, когда игрок наживает клавишу Enter или Escape. Если
нажат Enter, то мы возвращаем true, что указывает на то, что игрок выбрал цели.
При нажатии escape функция вернет false, что говорит нам о том, что выбор цели
отменен.
Если функция вернула true, то в TargetEnemy мы обрабатываем выбор цели.
dod.bas:TargetEnemy
'Игрок нажал enter. If ret = TRUE Then 'Проверим первый слот на наличие дистанционного оружия. If pchar.ProjectileEquipped(wPrimary) = TRUE Then 'Убедимся что заряжено. If pchar.IsLoaded(wPrimary) = TRUE Then 'Отобразим анимацию выстрела. level.AnimateProjectile source, target
Также, как мы это делали в LoadAmmo, нам нужно проверить что у персонажа
есть дистанционное оружие в первичном или вторичном слоте. Проверка одинакова
для обоих слотов, поэтому мы рассмотрим проверку только первичного слота.
Функция IsLoaded говорит нам о том, что оружие заряжено и готово к выстрелу.
Затем мы отображаем анимацию полета снаряда при помощи метода объекта уровня
AnimateProjectile.
map.bi
'Анимированный полет снаряда. Sub levelobj.AnimateProjectile (source As vec, target As vec) Dim As Integer x = source.vx, y = source.vy, d = 0, dx = target.vx - source.vx Dim As Integer dy = target.vy - source.vy, c, m, xinc = 1, yinc = 1 Dim As Integer delay = 10 If dx < 0 Then xinc = -1 dx = -dx Endif If dy < 0 Then yinc = -1 dy = -dy Endif If dy < dx Then c = 2 * dx m = 2 * dy Do While x <> target.vx _level.lmap(x, y).target.id = 7 _level.lmap(x, y).target.tcolor = fbYellow DrawMap Sleep delay _level.lmap(x, y).target.id = 0 _level.lmap(x, y).target.tcolor = fbBlack DrawMap x += xinc d += m If d > dx Then y += yinc d -= c Endif Loop Else c = 2 * dy m = 2 * dx Do While y <> target.vy _level.lmap(x, y).target.id = 7 _level.lmap(x, y).target.tcolor = fbYellow DrawMap Sleep delay _level.lmap(x, y).target.id = 0 _level.lmap(x, y).target.tcolor = fbBlack DrawMap y += yinc d += m If d > dy Then x += xinc d -= c Endif Loop Endif _level.lmap(x, y).target.id = 249 _level.lmap(x, y).target.tcolor = fbYellow DrawMap Sleep delay _level.lmap(x, y).target.id = 0 _level.lmap(x, y).target.tcolor = fbBlack DrawMap End Sub
Начальные координаты полета, это координаты персонажа, а конечные —
выбранная игроком цель. Здесь мы используем алгоритм Брезенхэма рисования линий
для обновления массива target объекта уровня и перерисовываем карту уровня. При
выполнении подпрограммы мы будем видеть анимацию снаряда, летящего к указанной
цели.
После анимации выстрела мы возвращаемся в TargetEnemy и уменьшаем количество зарядов в оружии.
dod.bas:TargetEnemy
'Отобразим анимацию выстрела. level.AnimateProjectile source, target 'Удаляем снаряд из оружия. pchar.DecrementAmmo wPrimary
Метод объекта персонажа DecrementAmmo удаляет один снаряд из оружия в
указанном слоте.
character.bi
'Уменьшим заряды в используемом оружии. Sub character.DecrementAmmo(slot As wieldpos) _cinfo.cwield(slot).weapon.ammocnt -= 1 If _cinfo.cwield(slot).weapon.ammocnt < 0 Then _cinfo.cwield(slot).weapon.ammocnt = 0 Endif End Sub
Как вы можете видеть, мы просто уменьшаем на 1 значение поля ammocnt у
объекта оружия.
Далее необходимо проверить, попали ли мы во что либо и выполнить соответствующие действия.
dod.bas:TargetEnemy
'Удаляем снаряд из оружия. pchar.DecrementAmmo wPrimary 'Проверим есть ли в координатах цели монстр. If level.IsMonster(target.vx, target.vy) = TRUE Then 'Нанесем ему повреждения. DoProjectileCombat target.vx, target.vy, wPrimary Else PrintMessage "No monster at target location." End If Else PrintMessage "Weapon is not loaded." Endif
Сначала мы проверяем, есть ли в указанных координатах монстр, и если это
так, то нанесем ему повреждения при помощи подпрограммы DoProjectileCombat.
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 'Получим фактор защиты монстра. df = level.GetMonsterDefense(mx, my) mname = level.GetMonsterName(mx, my) 'Здоровье монстра. mxp = level.GetMonsterXP(mx, my) 'Убедимся что что то есть. If df > 0 Then 'Возвращает боевой фактор в зависимости от того что держит персонаж. cf = pchar.GetProjectileCombatFactor() 'Получим случайные значения. 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 'Обновим здоровье монстра. 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
Здесь происходит тоже самое, что и при ближнем бое, за исключением того,
что используются значения для дистанционных а не контактных атак.
GetProjectileCombatFactor возвращает объединенные боевые факторы.
character.bi
'Возвращает боевой фактор для дистанционных атак. Function character.GetProjectileCombatFactor() As Integer Return CurrPCF + BonPcf End Function
Также как и для ближнего боя, мы получаем два случайных числа основанных
на боевых факторах и сравниваем их. Если персонаж попал в монстра то ему
наносится урон, зависящий от используемого оружия, за вычетом брони монстра.
Если монстр при этом погибает, то персонаж получает опыт и монстр удаляется из
игры.
Хотя реализация удаленных атак и кажется простой, но потребовалось написать довольно много кода, для того, чтобы все работало правильно. Но возможность стрельбы добавляет глубины в игру, и, поэтому, стоит усилий, потраченных на нее.
Перевод на русский: Fantik
содержание | назад | вперед