Давайте сделаем рогалик (Броня и щиты)
Следующим нашим шагом будет добавление в игру предметов брони и щитов. Также, чтобы они у нас работали, нам необходимо реализовать команду инвентаря «Взять в руки/Одеть». Тут мы сталкиваемся с небольшой проблемой, так как нам нужно уметь не только одевать предметы, но и снимать их. Так как мы имеем дело с двумя действиями, то имеет смысл реализовать для них две команды: «Взять в руки/Одеть» и «Снять». Этим мы и займемся в этой главе. Добавление новых предметов инвентаря — самая простая часть, так как у нас уже есть все методы работы с инвентарем и нам необходимо только добавить определение новых типов предметов и у нас будут щиты и броня. Тем не менее нам придется обновить объект персонажа для их хранения, а также существующие функции работы с инвентарем — для обработки новых типов предметов. Так что в у нас будет достаточно работы, но когда мы закончим, то добавление в игру новых предметов будет простой задачей. Давайте посмотрим на определение брони и щитов:
inv.bi
'Идентификаторы доспехов Enum armorids armArmorNone armCloth 'Плотная ткань. Cloth armor: поглощает 1% повреждений armLeather 'Кожаная. Leather armor: поглощает 5% повреждений armCuirboli 'Прошитая кожаная. Cuirboli armor: поглощает 10 % повреждений armRing 'Кожа с металлическими кольцами. Ring armor: поглощает 20% повреждений armBrigantine 'Полная кожаная? Brigantine armor: поглощает 30% повреждений armChain 'Кольчуга. Chain armor: поглощает 50 % повреждений armScale 'Чешуйчатая броня. Scale armor: поглощает 70% повреждений armPlate 'Пластинчатая броня. Plate armor: поглощает 90% повреждений End Enum 'Идентификаторы щитов Enum shieldids shldShieldNone shldLeather 'Значения для щитов такие же как и для доспехов. shldCuirboli shldRing shldBrigantine shldChain shldScale shldPlate End Enum
Это типы доспехов и щитов, которые будут у нас в игре. Как вы можете
видеть из комментариев, броня в подземелье судьбы ведет себя несколько иначе,
чем в других ролевых системах. Броня не дает способность персонажу перенести
удар полностью без последствий, а просто поглощает часть урона. Так или иначе,
повреждения, наносимые персонажу при ударе, зависят от различных факторов
защиты. Если персонажа ударили, то броня уменьшает повреждения на определенный
процент. Броня из ткани уменьшит его на 1%, а пластинчатые доспехи на 90%.
Однако, чем больше повреждений поглощает броня, тем больше у нее требования к
параметру силы персонажа для возможности ее использования. Также требование по
параметру силы персонажа зависит от генератора случайных чисел, так что удача
игрока здесь также немаловажный параметр.
inv.bi
'Слоты инвентаря на персонаже (предметы в руках и т.д.). Enum wieldpos wNone wPrimary wSecondary wArmor wNeck wRingRt wRingLt End Enum 'Описание типа для брони Type armortype id As armorids 'Тип брони evaldr As Integer 'Сложность опознания. Если больше > 0 то магический предмет. eval As Integer 'Если предмет опознан. effect As Integer 'Магический эффект (заклинание). sdesc As String * 30 'Секретное название для магических предметов. Отображается если он опознан. noise As Integer 'Производимый шум при использовании или если лежит в инвентаре. use As itemuse 'Как предмет используется. dampct As Single 'Процент поглощаемых повреждений. struse As Integer 'Сколько необходимо значение силы для использования. wslot(1 To 2) As wieldpos 'Слоты, занимаемые предметом. Может занимать одновременно до двух слотов. End Type 'Описание типа для щитов Type shieldtype id As shieldids 'Тип щита evaldr As Integer 'Сложность опознания. Ксли больше > 0 то магический. eval As Integer 'Если предмет опознан. effect As Integer 'Магический эффект. sdesc As String * 30 'Секретное название для магических предметов. Отображается если он опознан. noise As Integer 'Сколько шума производит при использовании и при нахождении в инвентаре. use As itemuse 'Как используется. dampct As Single 'Процент поглощаемых повреждений. struse As Integer 'Сила, необходимая для использования wslot(1 To 2) As wieldpos 'Слоты, занимаемые предметом. Может занимать одновременно до двух слотов. End Type
Описание типов для брони и щитов содержит довольно много информации.
Некоторые поля вы видели раньше, такие как идентификатор (id), опознание (eval),
генерируемый предметом шум (noise) и использование (use). Новое поле dampct
указывает на количество поглощаемых повреждений, struse — минимальное значение
параметры силы персонажа для возможности использования данного предмета и
массив, указывающий на занимаемые предметом слоты когда персонаж его использует.
Вы у видите как это работает когда мы дойдем до реализации команд «Взять в
руки/Одеть» и «Снять».
В Подземелье Судьбы магические броня и оружие содержат заклинания, которые на них могут быть наложены, а не просто являются магическими сами по себе, как это было у нас с едой. Поскольку мы еще не дошли до реализации магии, то пока просто зарезервируем в описании наших предметов место для заклинаний, которые они могу содержать и которое мы сможем заполнить позже, с развитием нашего проекта.
Для генерации новых типов предметов, нам придется обновить каждую из существующих подпрограмм в inv.bi. Процесс очень прост, для брони и щитов мы добавим новый раздел в каждую процедуру. Например, подпрограмма ClearInv обновляется путем добавления нового условия в секцию Select — Case.
inv.bi:ClearInv
Case clArmor inv.armor.id = armArmorNone inv.armor.evaldr = 0 inv.armor.eval = FALSE inv.armor.effect = 0 inv.armor.sdesc = "" inv.armor.noise = 0 inv.armor.use = UseNone inv.armor.dampct = 0 inv.armor.struse = 0 inv.armor.wslot(1) = wNone inv.armor.wslot(2) = wNone Case clShield inv.shield.id = shldShieldNone inv.shield.evaldr = 0 inv.shield.eval = FALSE inv.shield.effect = 0 inv.shield.sdesc = "" inv.shield.noise = 0 inv.shield.use = UseNone inv.shield.dampct = 0 inv.shield.struse = 0 inv.shield.wslot(1) = wNone inv.shield.wslot(2) = wNone
Другие подпрограммы обновляются подобным образом. Для создания доспехов
мы вызываем подпрограмму GenerateArmor.
inv.bi
'Создает предмет брони. Sub GenerateArmor(inv As invtype, currlevel As Integer, arid As armorids = armArmorNone) Dim item As armorids Dim As Integer isMagic = ItemIsMagic(currlevel) 'Общее для всех предметов. If arid = armArmorNone Then item = RandomRange(armCloth, armPlate) inv.armor.id = item Else item = arid inv.armor.id = item End If inv.icon = Chr(234) inv.iconclr = fbEmeraldGreen inv.armor.use = useWieldWear inv.armor.eval = FALSE inv.armor.wslot(1) = wArmor 'Магический предмет. If IsMagic = TRUE Then inv.armor.evaldr = RandomRange(currlevel, currlevel * 2) inv.armor.effect = 0 Endif 'Зададим тип брони и параметры. Select Case item Case armCloth inv.desc = "Cloth Armor" 'Cloth armor: 1% damage reduction. inv.armor.noise = 1 inv.armor.dampct = .01 inv.armor.struse = 10 Case armLeather 'Leather armor: 5% damage reduction inv.desc = "Leather Armor" inv.armor.noise = 5 inv.armor.dampct = .05 inv.armor.struse = 50 Case armCuirboli 'Cuirboli armor: 10 % damage reduction inv.desc = "Cuirboli Armor" inv.armor.noise = 10 inv.armor.dampct = .10 inv.armor.struse = 100 Case armRing 'Ring armor: 20% damage reduction inv.desc = "Ring Armor" inv.armor.noise = 15 inv.armor.dampct = .20 inv.armor.struse = 150 Case armBrigantine 'Brigantine armor: 30% dam reduction inv.desc = "Brigantine Armor" inv.armor.noise = 20 inv.armor.dampct = .30 inv.armor.struse = 200 Case armChain 'Chain armor: 50 % dam reduction inv.desc = "Chain Armor" inv.armor.noise = 25 inv.armor.dampct = .50 inv.armor.struse = 250 Case armScale 'Scale armor: 70% dam reduction inv.desc = "Scale Armor" inv.armor.noise = 30 inv.armor.dampct = .70 inv.armor.struse = 300 Case armPlate 'Plate armor: 90% dam reduction inv.desc = "Plate Armor" inv.armor.noise = 35 inv.armor.dampct = .90 inv.armor.struse = 350 End Select 'Зададим секретное описание. If IsMagic = TRUE Then inv.armor.sdesc = inv.desc Else inv.armor.sdesc = inv.desc Endif End Sub
Данная процедура почти идентична процедуре генерации предметов еды, но
генерирует параметры для доспехов. В нее же мы могли добавить и генерацию щитов.
Так как щиты у нас это просто еще один вид брони, но для наглядности я создал
для брони и щитов две разных функции чтобы лучше показать ход выполнения
программы. Посмотрев на код в inv.bi вы обнаружите, что точно такой же алгоритм
используется во всех подпрограммах работы с инвентарем. По аналогии с тем, как
мы добавили броню, мы будем добавлять в игру все новые предметы. Которые бы нам
ни потребовались.
После того, как броня и щиты созданы и размещены на карте, мы должны дать игроку возможность их использовать, что подводит нас к реализации команд инвентаря «Одеть» и «Снять».
В подпрограмме ManageInventory файла dod.bas для данных команд мы добавили новую возможность выбора.
dod.bas
'«Одеть/Взыть в руки» предмет. If kch = "U" Then ret = ProcessUnequip() 'Screen changed. If ret = TRUE Then DrawInventoryScreen Endif Endif '«Снять». If kch = "Q" Then ret = ProcessEquip() 'Screen changed. If ret = TRUE Then DrawInventoryScreen Endif Endif
Содержание этих двух новых подпрограмм аналогично уже существующим
подпрограммам инвентаря, поэтому их код должен быть вам знаком.
dod.bas
'Реализация команды «Одеть/Взять в руки». Function ProcessEquip() As Integer Dim As String res, mask, desc Dim As Integer i, iret, iitem, idx, ret = FALSE Dim As invtype inv Dim As tWidgets.btnID btn Dim As tWidgets.tInputbox ib Dim As vec mvec Dim slot As Integer 'Убедимя что в инвентаре есть что одеть. For i = pchar.LowInv To pchar.HighInv iitem = pchar.HasInvItem(i) If iitem = TRUE Then 'Получим предмет инвентаря. pchar.GetInventoryItem i, inv 'Если предмет можно одеть взять. iret = MatchUse(inv, useWieldWear) 'Добавим его в возможность выбора. If iret = TRUE Then 'Построим маску. mask &= Chr(i) End If Endif Next If Len(mask) = 0 Then ShowMsg "Equip Items", "Nothing to equip.", tWidgets.MsgBoxType.gmbOK Else 'Нарисовать поле ввода. ib.Title = "Equip Items" ib.Prompt = "Select item(s) to equip (" & mask & ")" ib.Row = 39 ib.EditMask = mask ib.MaxLen = Len(mask) ib.InputLen = Len(mask) btn = ib.Inputbox(res) 'Если игрок что то выбрал. If (btn <> tWidgets.btnID.gbnCancel) And (Len(res) > 0) Then 'Переберем все предметы. For i = 1 To Len(res) iitem = Asc(res, i) 'Get index into wield inventory. 'Получим предмет инвентаря. pchar.GetInventoryItem iitem, inv 'Получим описание предмета. desc = GetInvItemDesc(inv) iret = FALSE idx = 0 'Проверим первый слот. slot = GetInvWSlot(inv, 1) If slot <> wNone Then 'Проверим, что данный слот персонажа не занят. If pchar.HasInvItem(slot) = FALSE Then idx = slot iret = TRUE Endif Else 'Проверим второй слот. slot = GetInvWSlot(inv, 2) If slot <> wNone Then 'Проверим, что данный слот персонажа не занят. If pchar.HasInvItem(slot) = FALSE Then idx = slot iret = TRUE Endif End If Endif 'Есть свободное место? If iret = TRUE Then 'Убедимся что персонаж может использовать предмет. If pchar.CanWear(inv) = TRUE Then 'Поместим предмет на «одеваемую» позизию. pchar.AddInvItem idx, inv 'Уберем его из инвентаря. ClearInv inv 'Обновим состояние слотов. pchar.AddInvItem iitem, inv ret = TRUE desc &= " was equipped." ShowMsg "Equip Items", desc, tWidgets.MsgBoxType.gmbOK Else ShowMsg "Equip Items", "Not enough strength to equip " & desc & ".",tWidgets.MsgBoxType.gmbOK End If Else 'Нет свободного места. desc = "No empty slots to equip item." ShowMsg "Equip Items", desc, tWidgets.MsgBoxType.gmbOK Exit For Endif Next Endif Endif Return ret End Function
Так же, как мы делали в подпрограмме опознания предметов, мы проверяем
предметы из инвентаря — какие из них могут быть одеты или взяты в руки
персонажем. Как только список таких предметов сформирован, мы предоставляем го
пользователю, чтобы он смог сделать выбор. После того, как игрок выбрал предмет,
мы должны проверить в какой слот этот предмет может быть размещен и не занят ли
он уже. Каждый из «одеваемых» предметов может занимать определенное место в
слотах персонажа, которое описано перечислением, определенным в inv.bi. Мы
получаем этот слот вызывая функцию GetInvWSlot.
inv.bi
'Возвращает слоты инвентаря для одеваемых предметов. Function GetInvWSlot(inv As invtype, slotnum As Integer) As Integer Dim As Integer ret = wNone If inv.classid = clArmor Then If slotnum >= Lbound(inv.armor.wslot) And slotnum <= Ubound(inv.armor.wslot) Then ret = inv.armor.wslot(slotnum) End If Elseif inv.classid = clShield Then If slotnum >= Lbound(inv.shield.wslot) And slotnum <= Ubound(inv.shield.wslot) Then ret = inv.shield.wslot(slotnum) End If Endif Return ret End Function
Параметр slotnum является индексом массива wslot в описании типов для
доспехов и щитов. У нас каждый предмет может находится максимум в каких либо 2-х
разных слотах, например оружие — может находится в правой и в левой руке
(первичный и вторичный слот). Для того, чтобы получить слот в котором должен
быть размещен предмет мы передаем индекс в эту функцию. Доспехи у нас могут
располагаться только в слоте для доспехов, поэтому у них в массиве wslot по
индексу 1 будет содержаться wArmor, по индексу 2 — wNone или 0. Функция
возвращает wNone — если слот не задан в массиве wslot.
Что означают цифры указывающие на слот? Это индекс в массиве слотов содержащемся в объекте персонажа.
character.bi
cwield(wPrimary To wRingLt) As invtype 'Активный предмет: 1 = главное оружие, 2 = второе оружие/щит, 3 = броня, 4 = ожерелье, 5 = кольцо пр., 6 = кольцо лр.
Также они используются для меню выбора одеваемого/снимаемого предмета
инвентаря, как это показано на изображении в начале главы. Связав меню выбора
предмета с реальным размером массива мы упрощаем выбор слота одеваемого или
снимаемого предмета. Мы можем наблюдать это в коде функции ProcessEquip который
«одевает» предмет.
dod.bas
'проверим слот 1. slot = GetInvWSlot(inv, 1) If slot <> wNone Then 'Проверим персонажа чтобы убедится что слот свободен. If pchar.HasInvItem(slot) = FALSE Then idx = slot iret = TRUE Endif Else 'Проверим слот 2. slot = GetInvWSlot(inv, 2) If slot <> wNone Then 'Проверим персонажа чтобы убедится что слот свободен . If pchar.HasInvItem(slot) = FALSE Then idx = slot iret = TRUE Endif End If Endif
Обратите внимание, что мы используем индекс, получаемый из GetInvWSlot
чтобы использовать его в функции персонажа HasInvItem, которую мы обновили для
проверки: занят или свободен конкретный слот для «одеваемых» предметов.
character.bi
'Возвращает True если предмет находится в инвентаре. Function character.HasInvItem(idx As Integer) As Integer 'Проверим одетые предметы. If idx >= Lbound(_cinfo.cwield) And idx <= Ubound(_cinfo.cwield) Then 'Проверим идентификатор класса предмета. If _cinfo.cwield(idx).classid = clNone Then Return FALSE Else Return TRUE Endif Else 'Проверка индекса. If idx >= Lbound(_cinfo.cinv) And idx <= Ubound(_cinfo.cinv) Then 'Проверка идентификатора класса. If _cinfo.cinv(idx).classid = clNone Then Return FALSE Else Return TRUE Endif Else Return FALSE End If Endif End Function
Первый оператор If проверяет, попадает ли индекс в диапазон массива
одеваемых на персонажа предметов. Поскольку id занимаемого места у нас
соответствует индексу массива слотов, то далее мы просто проверяем массив по
этому индексу чтобы узнать — занят слот или свободен. Это соответствие индексов
расположения и индексов массива слотов позволяет упростить код и сделать его
более понятным и простым в отладке и обслуживании.
Если слот пуст, то мы просто помещаем в него предмет и удаляем его из массива инвентаря персонажа.
dod.bas:ProcessEquip
'Пустой слот найден? If iret = TRUE Then 'Убедимся, может ли персонаж пользоваться предметом. If pchar.CanWear(inv) = TRUE Then 'Поместим в «одеваемый» слот. pchar.AddInvItem idx, inv 'Удалим из инвентаря. ClearInv inv 'Обновим «одеваемые» слоты. pchar.AddInvItem iitem, inv ret = TRUE desc &= " was equipped." ShowMsg "Equip Items", desc, tWidgets.MsgBoxType.gmbOK Else ShowMsg "Equip Items", "Not enough strength to equip " & desc & ".",tWidgets.MsgBoxType.gmbOK End If Else 'Нет свободного слота. desc = "No empty slots to equip item." ShowMsg "Equip Items", desc, tWidgets.MsgBoxType.gmbOK Exit For Endif
Перед тем как одеть предмет, необходимо проверить — достаточно ли у
персонажа показатель силы для использования данного щита или доспехов. Для
данной проверки мы добавили новую функцию в объект персонажа: CanWear.
character.bi
'Возвращает True если персонаж может использовать предмет Function character.CanWear(inv As invtype) As Integer Dim As Integer ret = TRUE If inv.classid = clArmor Then If inv.armor.struse > _cinfo.stratt(0) Then ret = FALSE Endif Endif If inv.classid = clShield Then If inv.shield.struse > _cinfo.stratt(0) Then ret = FALSE Endif Endif Return ret End Function
Здесь у брони и щитов проверяется поле struse, для того чтобы определить
— достаточно ли сил у персонажа. Обратите внимание, что возвращаемое по
умолчанию значение — True. Только предметы определенного типа могут изменить
значение на False (в нашем случае броня и щиты), а для всех остальных мы будем
возвращать True.
Для команды «снять» мы делаем противоположные действия.
dod.bas
'Обработаем команду «снять». Function ProcessUnEquip() As Integer Dim As String res, mask, desc Dim As Integer i, iret, iitem, ret = FALSE Dim As invtype inv Dim As tWidgets.btnID btn Dim As tWidgets.tInputbox ib Dim As vec mvec 'Проверим предметы которые можно снять. For i = wPrimary To wRingLt iitem = pchar.HasInvItem(i) If iitem = TRUE Then 'Построим маску. mask &= Str(i) Endif Next If Len(mask) = 0 Then ShowMsg "Unequip Items", "Nothing to unequip.", tWidgets.MsgBoxType.gmbOK Else 'Нарисуем поле для ввода. ib.Title = "Unequip Items" ib.Prompt = "Select item(s) to unequip (" & mask & ")" ib.Row = 39 ib.EditMask = mask ib.MaxLen = Len(mask) ib.InputLen = Len(mask) btn = ib.Inputbox(res) 'Проверим каждый предмет из списка. If (btn <> tWidgets.btnID.gbnCancel) And (Len(res) > 0) Then 'Переберем список предметов. For i = 1 To Len(res) iitem = Val(Mid(res, i, 1)) 'Получим индекс слота. 'Получим предмет инвентаря. pchar.GetInventoryItem iitem, inv 'Получим описание. desc = GetInvItemDesc(inv) 'Ищем пустой слот инвентаря. iret = pchar.GetFreeInventoryIndex 'Нашелся пустой слот? If iret > -1 Then 'Поместим предмет в инвентарь. pchar.AddInvItem iret, inv 'Очистим предмет. ClearInv inv 'Обновим одеваемые слоты. pchar.AddInvItem iitem, inv ret = TRUE desc &= " was unequipped." ShowMsg "Unequip Items", desc, tWidgets.MsgBoxType.gmbOK Else 'Нет места в инвентаре. desc = "No empty inventory slots to unequip item." ShowMsg "Unequip Items", desc, tWidgets.MsgBoxType.gmbOK Exit For Endif Next Endif Endif Return ret End Function
Вместо того, чтобы перебирать предметы из инвентаря персонажыа, мы
перебираем массив «одеваемых» слотов: For i = wPrimary To wRingLt. Вызывая
HasInvItem, мы проверяем — занят ли слот, и если занят, то добавляем предмет в
список. Другое отличие здесь в том, что вместо использования ASCII колов мы
используем в маске фактические номера слотов: mask &= Str(i).
После того как сок список создан и игрок сделал свой выбор, мы обрабатываем его почти также, как и список выбрасываемых предметов. Опять же, разница в том, что мы должны обрабатывать номер индекса массива а не ASCII код: iitem = Val(Mid(res, i, 1)). За исключением этих отличий, алгоритм почти такой же, как и в команде «выбросить», мы перемещаем предмет из слота одетых предметов в слот инвентаря персонажа. Используя функцию GetFreeInventoryIndex мы получаем индекс свободного слота в инвентаре персонажа, так же, как мы это делали в команде «Поднять». Свободный слот нам нужен для того. Чтобы положить туда снятый с персонажа предмет.
Теперь нам необходимо отобразить на экране одетые на персонажа предметы. Мы обновили код в DrawMainScreen чтобы любые одетые предметы отображались на информационной панели.
dod.bas
'Проверим одетые предметы. If pchar.HasInvItem(wPrimary) = TRUE Then pchar.GetInventoryItem wPrimary, inv idesc = GetInvItemDesc(inv) Else idesc = "" Endif PutText "Primary: " & idesc, row, col row += 1 If pchar.HasInvItem(wSecondary) = TRUE Then pchar.GetInventoryItem wSecondary, inv idesc = GetInvItemDesc(inv) Else idesc = "" Endif PutText "Secondary: " & idesc, row, col row += 1 If pchar.HasInvItem(wArmor) = TRUE Then pchar.GetInventoryItem wArmor, inv idesc = GetInvItemDesc(inv) Else idesc = "" Endif PutText "Armor: " & idesc, row, col row += 1 If pchar.HasInvItem(wNeck) = TRUE Then pchar.GetInventoryItem wNeck, inv idesc = GetInvItemDesc(inv) Else idesc = "" Endif PutText "Neck: " & idesc, row, col row += 1 If pchar.HasInvItem(wRingRt) = TRUE Then pchar.GetInventoryItem wRingRt, inv idesc = GetInvItemDesc(inv) Else idesc = "" Endif PutText "Ring RT: " & idesc, row, col row += 1 If pchar.HasInvItem(wRingLt) = TRUE Then pchar.GetInventoryItem wRingLt, inv idesc = GetInvItemDesc(inv) Else idesc = "" Endif PutText "Ring LT: " & idesc, row, col
Здесь мы просто проверяем все слоты для одетых предметов и если какой
либо слот не пуст, то получаем название предмета который в нем находится и
выводим его на экран.
Последнее что нам осталось сделать, это чтобы персонаж начинал игру с какой нибудь броней. Для этого необходимо добавить код в подпрограмму GenerateCharacter.
character.bi
'Добавим персонаджу броню из обычной ткани. inv.classid = clArmor GenerateArmor inv, 1, armCloth SetInvEval inv, TRUE AddInvItem wArmor, inv 'Добавим персонажу нож
Как только персонаж создан, мы создаем для него доспехи из ткани и
помещаем в слот для доспехов. Обратите внимание, что мы отметили место вставки
кода для добавление персонажу ножа. Наш персонаж будет начинать с ним игру,
чтобы не быть совсем уж беззащитным.
Теперь у нас есть доспехи, но еще нам необходимо и оружие, которое мы добавим в следующей главе.
Перевод на русский: Fantik
содержание | назад | вперед