Давайте сделаем рогалик (Вызов заклинаний)
Теперь,когда у нас есть изученные заклинания, перейдем к их использованию персонажем. Чтобы вызвать какое либо заклинание, игрок использует клавишу «c» которая обрабатывается следующим кодом в основном цикле игры:
dod.bas
'Вызов заклинания. If ckey = "c" Then CastSpell level.MoveMonsters DrawMainScreen Endif
Как вы можете видеть, мы вызываем новую подпрограмму CastSpell, которую добавили в файл dod.bas
dod.bas'Вызов заклинания. Sub CastSpell () Dim As Integer splcnt, i, iitem, ret, mc, md, rollm, rollp Dim As Integer tmp, snd, cancel = FALSE Dim As invtype sinv, iinv Dim As tWidgets.listtype splist() Dim As tWidgets.btnID btn Dim As tWidgets.tList lst Dim As vec vt, pt Dim tid As terrainids 'Проверим список заклинаний. For i = pchar.LowISpell To pchar.HighISpell iitem = pchar.HasInvItem(i) If iitem = TRUE Then 'Получим предмет инвентаря. pchar.GetInventoryItem i, sinv 'Добавим название заклинания в список. splcnt += 1 Redim Preserve splist(1 To splcnt) 'Используем индекс в качестве id. splist(splcnt).id = i splist(splcnt).text = sinv.spell.splname Endif Next 'Убедимся, что есть заклинание. If splcnt > 0 Then 'Установим id равное 0. iitem = 0 'Настроим окно со списком. lst.Title = "Select Spell to Cast" lst.Prompt = "Use Up or Dn key to cycle spells, Enter to select." 'Получим выбор игрока. btn = lst.Listbox(splist(), iitem) 'Проверим, что игрок не нажал отмену. If (btn <> tWidgets.gbnCancel) And (iitem <> 0) Then 'Получим предмет инвентаря, используя полученный id. pchar.GetInventoryItem iitem, sinv 'Убедимся что у персонажа достаточно маны. If sinv.spell.manacost > pchar.CurrMana Then ShowMsg "Mana", "You do not have enough Mana to cast spell.", tWidgets.MsgBoxType.gmbOK Else 'Проверим цель заклинания. ret = splSet.IsMember(sinv.spell.id) 'This is a target spell. If ret = TRUE Then 'Получим координаты цели. ret = GetTargetCoord(vt) 'Убедимся, что игрок указал цель. If ret = TRUE Then 'Уьедимся в правильности цели. If level.IsMonster(vt.vx, vt.vy) = FALSE Then ShowMsg "Target", "Nothing to target!", tWidgets.MsgBoxType.gmbOK Else 'Нарисуем магическую атаку. pt.vx = pchar.Locx pt.vy = pchar.Locy level.AnimateProjectile pt, vt 'Получим фактор магической защиты монстра. md = level.GetMonsterMagicDefense(vt.vx, vt.vy) 'Получим фактор магической атаки персонажа. mc = pchar.CurrMcf + pchar.BonMcf 'Возьмем случайные значения. rollp = RandomRange(1, mc) 'атака rollm = RandomRange(1, md) 'защита 'Персонаж поразил цель? If rollp > rollm Then 'Применим эффект заклинания на монстра. ret = level.ApplySpell(sinv.spell, vt.vx, vt.vy) Else 'Сообщение о промахе. PrintMessage "The " & level.GetMonsterName(vt.vx, vt.vy) & " dispels the " & sinv.spell.splname & "!" Endif Endif End If Else Select Case sinv.spell.id Case splHeal tmp = pchar.MaxHP * (sinv.spell.lvl / 100) If tmp < 1 Then tmp = 1 pchar.CurrHP = pchar.CurrHP + tmp Case splMana tmp = pchar.MaxMana * (sinv.spell.lvl / 100) If tmp < 1 Then tmp = 1 pchar.CurrMana = pchar.CurrMana + tmp Case splRecharge 'Найдем палочки в инвертаре и зарядим в соответствии с уровнем. For i = pchar.LowInv To pchar.HighInv iitem = pchar.HasInvItem(i) If iitem = TRUE Then 'Получим предметы инвентаря. pchar.GetInventoryItem i, iinv 'Проверим, оружие ли это. If iinv.classid = clWeapon Then 'Проверим — палочка ли это. If iinv.weapon.iswand = TRUE Then 'Увеличим заряды. iinv.weapon.ammocnt += sinv.spell.lvl 'Убедимся, что не превысили максимальное кол-во зарядов. If iinv.weapon.ammocnt > iinv.weapon.capacity Then iinv.weapon.ammocnt = iinv.weapon.capacity Endif 'Вернем предмет в инвентарь. pchar.AddInvItem iitem, iinv Endif Endif End If Next 'Проверим в слотах экипировки и тоже зарядим, если возможно iitem = pchar.HasInvItem(wPrimary) If iitem = TRUE Then pchar.GetInventoryItem wPrimary, iinv If iinv.classid = clWeapon Then 'Проверим — палочка ли это. If iinv.weapon.iswand = TRUE Then iinv.weapon.ammocnt += sinv.spell.lvl If iinv.weapon.ammocnt > iinv.weapon.capacity Then iinv.weapon.ammocnt = iinv.weapon.capacity Endif pchar.AddInvItem wPrimary, iinv Endif Endif Endif 'Проверим следующий слот. iitem = pchar.HasInvItem(wSecondary) If iitem = TRUE Then pchar.GetInventoryItem wSecondary, iinv If iinv.classid = clWeapon Then 'Проверим — палочка ли это. If iinv.weapon.iswand = TRUE Then iinv.weapon.ammocnt += sinv.spell.lvl If iinv.weapon.ammocnt > iinv.weapon.capacity Then iinv.weapon.ammocnt = iinv.weapon.capacity Endif pchar.AddInvItem wSecondary, iinv Endif Endif Endif Case splFocus 'Увеличим все боевые факторы на 1 ход. pchar.BonUcf = sinv.spell.lvl pchar.BonUcfCnt = 1 pchar.BonAcf = sinv.spell.lvl pchar.BonAcfCnt = 1 pchar.BonPcf = sinv.spell.lvl pchar.BonPcfCnt = 1 pchar.BonCdf = sinv.spell.lvl pchar.BonCdfCnt = 1 pchar.BonMcf = sinv.spell.lvl pchar.BonMcfCnt = 1 pchar.BonMdf = sinv.spell.lvl pchar.BonMdfCnt = 1 Case splTeleport 'Получим координаты цели. ret = GetTargetCoord(vt, sinv.spell.lvl) If ret = TRUE Then 'Проверим, есть ли монстр. If level.IsMonster(vt.vx, vt.vy) = TRUE Then 'Телепорт в монстра убивает его. ret = level.ApplySpell(sinv.spell, vt.vx, vt.vy) 'Установим новые координаты персонажа. pchar.Locx = vt.vx pchar.Locy = vt.vy 'Сгенерируем карту звуков. level.ClearSoundMap snd = pchar.GetNoise() level.GenSoundMap(pchar.Locx, pchar.Locy, snd) Else 'Убедимся, что тайл карты не блокирован. If level.IsBlocking(vt.vx, vt.vy) = FALSE Then 'Установим новые координаты персонажа. pchar.Locx = vt.vx pchar.Locy = vt.vy 'Сгенерируем карту звуков. level.ClearSoundMap snd = pchar.GetNoise() level.GenSoundMap(pchar.Locx, pchar.Locy, snd) Else ShowMsg "Teleport", "You can't teleport there.", tWidgets.MsgBoxType.gmbOK cancel = TRUE End If Endif Else cancel = TRUE Endif Case splOpen 'Получим координаты цели. ret = GetTargetCoord(vt, sinv.spell.lvl) If ret = TRUE Then 'Получим id местности. tid = level.GetTileID(vt.vx, vt.vy) 'Проверим на наличие закрытой двери. If tid = tDoorClosed Then 'Проверим, заперта ли она. If level.IsDoorLocked(vt.vx, vt.vy) = TRUE Then ret = level.OpenLockedDoor(vt.vx, vt.vy, sinv.spell.lvl) If ret = TRUE Then PrintMessage "Door was opened." Endif Else 'Сообщим игроку, что дверь не щаперта. ShowMsg "Open Spell", "The door is not locked.", tWidgets.MsgBoxType.gmbOK cancel = TRUE Endif Endif Else cancel = TRUE End If Case splBlink 'Установим эффект ослепления. pchar.SetSpellEffect sinv.spell.id, sinv.spell.lvl, 0 End Select Endif If cancel = FALSE Then 'Уменьшим ману на стоимость заклинания. pchar.CurrMana = pchar.CurrMana - sinv.spell.manacost End If Endif Endif Else ShowMsg "Spells", "You have not learned any spells.", tWidgets.MsgBoxType.gmbOK Endif End Sub
Как вы можете видеть, тут много чего происходит. Однако в большинстве случаев, мы просто получаем необходимую для заклинания информацию и вызываем соответствующую подпрограмму. Давайте рассмотрим каждый раздел подробнее, чтобы получить лучшее представление о том, что тут происходит.
dod.bas:CastSpell... 'Проверим список заклинаний. For i = pchar.LowISpell To pchar.HighISpell iitem = pchar.HasInvItem(i) If iitem = TRUE Then 'Получим предмет инвентаря. pchar.GetInventoryItem i, sinv 'Добавим название заклинания в список. splcnt += 1 Redim Preserve splist(1 To splcnt) 'Используем индекс в качестве идентификатора. splist(splcnt).id = i splist(splcnt).text = sinv.spell.splname Endif Next ...
В этом For-Next цикле, мы проверяем все слоты инвентаря заклинаний на наличие в них заклинаний. Мы используем два новых свойства объекта персонажа, pchar.LowISpell и pchar.HighISpell, которые содержат значения минимального и максимального индекса массива заклинаний.
character.bi'Возвращает минимальный индекс массива заклинаний. Property character.LowISpell() As Integer Return Lbound(_cinfo.cspells) End Property 'Возвращает максимальный индекс массива заклинаний. Property character.HighISpell() As Integer Return Ubound(_cinfo.cspells) End Property
В цикле, при помощи функции HasInvItem мы проверяем, содержится ли в данном слоте заклинание, и если это так, то получаем его при помощи функции GetInventoryItem. В следующей части кода мы копируем информацию о полученном объекте в SPList, тип которого определен в tWidgets объекте. Этот список будет передан объекту tWidgets для отображения списка заклинаний, чтобы дать возможность игроку выбрать заклинание из этого списка.
dod.bas:CastSpell... 'Убедимся, что есть заклинание. If splcnt > 0 Then 'Установим id равное 0. iitem = 0 'Настроим окно со списком. lst.Title = "Select Spell to Cast" lst.Prompt = "Use Up or Dn key to cycle spells, Enter to select." 'Получим выбор игрока. btn = lst.Listbox(splist(), iitem) 'Убедимся, что игрок не нажал на отмену. If (btn <> tWidgets.gbnCancel) And (iitem <> 0) Then 'Получим предмет инвентаря, в соответствии с полученным идентификатором. pchar.GetInventoryItem iitem, sinv 'Убедимся, что у персонажа достаточно маны. If sinv.spell.manacost > pchar.CurrMana Then ShowMsg "Mana", "You do not have enough Mana to cast spell.", tWidgets.MsgBoxType.gmbOK Else ...
Сначала мы должны проверить, что у нас есть заклинания для отображения. Для этого мы используем переменную splcnt. Если заклинания найдены, то мы отображаем список заклинаний, содержащийся в splist(). Игрок будет иметь возможность выбрать заклинание из списка, или нажать Escape для отмены (Объект List будет рассмотрен в приложении, наряду с другими объектами tWidget).
Если игрок выберет заклинание, то мы получаем его используя возвращенный индекс , хранящийся в переменной iitem. После мы должны проверить, достаточно ли у персонажа маны для использования выбранного заклинания, так как большинство заклинаний требую ее определенное количество.
dod.bas:CastSpell...
'Проверим цель заклинания.
ret = splSet.IsMember(sinv.spell.id)
...
Существует два типа заклинаний, одни наносят ущерб противнику, другие, наоборот, улучшают состояние заклинателя, например исцеление или улучшение боевых характеристик. Обратите внимание, что мы создали новый объект для хранения идентификаторов атакующих заклинаний, и, при помощи функции IsMember, мы проверяем, содержится ли выбранное заклинание в этом списке. Новый объект находится в файле set.bi.
set.bi'Проверим на существующие определения. #IFNDEF NULL #DEFINE NULL 0 #ENDIF #IFNDEF FALSE #DEFINE FALSE 0 #DEFINE TRUE (Not FALSE) #ENDIF Type setobj Private: _set As Integer Ptr 'Набор элементов. _setcnt As Integer 'Кол-во элементов в наборе. Declare Sub _DestroySet() 'Очистить объект и освободить память. Public: Declare Constructor () Declare Destructor () Declare Function AddToSet (item As Integer)As Integer 'Вернет TRUE если объект добавлен. Declare Function IsMember(item As Integer) As Integer 'Вернет TRUE если объект в наборе. End Type 'Очищает объекты и освобождает память. Sub setobj._DestroySet() If _set <> NULL Then Deallocate _set _set = NULL Endif End Sub 'Конструктор ничего не делает в данный момент. Constructor setobj () _DestroySet End Constructor 'Деструктор, очищает объекты. Destructor setobj () _DestroySet End Destructor 'Возвращает TRUE если предмет добавлен. Function setobj.AddToSet (item As Integer)As Integer Dim As Integer ret = TRUE If _set = NULL Then _setcnt += 1 _set = Callocate(_setcnt, Sizeof(Integer)) _set[_setcnt - 1] = item Else 'Проверим на присутствие предмета. For i As Integer = 0 To _setcnt - 1 If _set[i] = item Then ret = FALSE Exit For Endif Next 'Если не нашли. If ret = TRUE Then _setcnt += 1 _set = Reallocate(_set, _setcnt * Sizeof(Integer)) If _set <> NULL Then _set[_setcnt - 1] = item Endif Endif Endif Return ret End Function 'Возвращает TRUE если предмет в наборе. Function setobj.IsMember(item As Integer) As Integer Dim As Integer ret = FALSE If _set <> NULL Then For i As Integer = 0 To _setcnt - 1 If _set[i] = item Then ret = TRUE Exit For Endif Next Endif Return ret End Function
Этот объект достаточно прост. В приватных переменных он содержит массив с целочисленных указателей, конструктор и деструктор — который освобождает используемую память.
Метод AddToSet добавляет объект в набор. Мы используем массив указателей, так как не можем использовать динамические массивы в определении типа и должны сами управлять используемой памятью. AddToSet перераспределяет память расширяя массив указателей и добавляет в него новый элемент. Но прежде чем это сделать, мы должны убедиться, что в наборе уже не присутствует данные элемент, так как все элементы должны быть уникальными и добавление дублирующихся объектов в любом случае не имеет смысла. Основная цель сохранения объектов в наборе, это определение того, что какой либо предмет принадлежит определенной коллекции, как, например, мы делаем это в подпрограмме CastSpell. Разумеется, изначально мы должны заполнить набор элементами, что мы делаем в процедуре InitTargetSpells.
inv.bi'Инициализируем коллекцию заклинаний, для которых необходимо указание цели. Sub InitTargetSpells() Dim As Integer ret 'Добавим заклинания в коллекцию. ret = splSet.AddToSet(splAcidFog) ret = splSet.AddToSet(splFireCloak) ret = splSet.AddToSet(splLightning) ret = splSet.AddToSet(splBlind) ret = splSet.AddToSet(splFear) ret = splSet.AddToSet(splConfuse) ret = splSet.AddToSet(splFireBomb) ret = splSet.AddToSet(splEntangle) ret = splSet.AddToSet(splCloudMind) ret = splSet.AddToSet(splFireball) ret = splSet.AddToSet(splIceStatue) ret = splSet.AddToSet(splRust) ret = splSet.AddToSet(splShatter) ret = splSet.AddToSet(splMagicDrain) ret = splSet.AddToSet(splEnfeeble) ret = splSet.AddToSet(splStealHealth) ret = splSet.AddToSet(splMindBlast) End Sub
Как вы можете видеть, мы добавили все заклинания, для которых необходимо указание цели. Эта подпрограмма вызывается из основной программы во время инициализации приложения.
dod.bas'Используем разрешение 640x480 32bit с текстом 80x60 символов. Screenres sw, sh, 32 Width txcols, txrows Windowtitle "Dungeon of Doom" Randomize Timer 'Установим генератор случайных чисел. tWidgets.InitWidgets 'Инициализируем виджеты. 'Инициализируем список заклинаний для которых необходимо указание цели. InitTargetSpells 'Нарисуем титульный экран. DisplayTitle
Сам объект задан в файле defs.bi как общая (Shared) переменная.
defs.bi'Список сообщений. Dim Shared mess(1 To 4) As String Dim Shared messcolor(1 To 4) As Uinteger = {fbWhite, fbWhite1, fbWhite2, fbWhite3} 'Набор заклинаний. Dim Shared splSet As setobj
Это может показаться излишним — создавать отдельный объект для набора значений, хотя мы могли просто создать массив с этими значениями. Но что если в определенный момент нам понадобиться другой набор, или много разных наборов. Мы могли бы использовать полдюжины различных массивов для этих целей, но выглядело бы это «грязно». С объектом, же, независимо от того, сколько наборов нам потребуется, мы просто проинициализируем объект новыми значениями. Даже если нам не понадобятся больше коллекции элементов, обхем кода для описания этого объекта не превышает объем кода, который бы нам пришлось написать для работы с предопределенным массивом. Поэтому, в любом случае, лучше использовать данный объект.
Возвращаясь к CastSpell: когда мы определим, что для выбранного заклинания необходимо указать цель, мы должны эту цель получить.
dod.bas:CastSpell... 'Получим координаты цели. ret = GetTargetCoord(vt) 'Убедимся, что игрок выбрал цель. If ret = TRUE Then 'Убедимся что цель верная. If level.IsMonster(vt.vx, vt.vy) = FALSE Then ShowMsg "Target", "Nothing to target!", tWidgets.MsgBoxType.gmbOK Else 'Отобразим анимацию атаки. pt.vx = pchar.Locx pt.vy = pchar.Locy level.AnimateProjectile pt, vt ...
Мы будем использовать тот же код, который написали для реализации выбора цели дистанционных атак. Использовать повторно уже работающий код — всегда хорошая идея. Также, как и для дистанционных атак, мы нарисуем анимацию атаки, что обеспечит некоторую обратную связь, и даст игроку понять, что заклинание на самом деле сработало.
dod.bas:CastSpell... 'Получим фактор магической защиты монстра. md = level.GetMonsterMagicDefense(vt.vx, vt.vy) 'Получим фактор магической атаки персонажа. mc = pchar.CurrMcf + pchar.BonMcf 'Получим случайные значения. rollp = RandomRange(1, mc) 'offense rollm = RandomRange(1, md) 'defense 'Персонаж попал по цели? If rollp > rollm Then 'Назначим эффект заклинания монстру. ret = level.ApplySpell(sinv.spell, vt.vx, vt.vy) Else 'Сообщение о промахе. PrintMessage "The " & level.GetMonsterName(vt.vx, vt.vy) & " dispels the " & sinv.spell.splname & "!" Endif Endif End If ...
В данной секции мы проверяем — попало заклинание в цель или нет. Мы сравниваем два случайных числа, полученный в соответствии с факторами магической защиты монстра и магической атаки персонажа. Если у монстра выпало меньшее случайное число, то персонаж попал заклинанием в цель. Если персонаж попадает, то мы передаем информацию о заклинании в функцию ApplySpell объекта уровня подземелья. Мы создали ApplySpell во время реализации магии предметов, так что теперь нам просто необходимо добавить в нее атакующие заклинания.
map.biCase splStealHealth ret = ApplyDamage(mx, my, dam) pchar.CurrHP = pchar.CurrHP + dam txt = "Steal Health Spell stole " & dam & " health from " & _level.moninfo(midx).mname & "." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splLightning ret = ApplyDamage(mx, my, spl.dam) txt = "Lightning Spell inflicted " & spl.dam & " damage to " & _level.moninfo(midx).mname & "." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splAcidFog 'КнигаЗакл.: 5 повреждений каждые lvl ходов ret = ApplyDamage(mx, my, dam) 'Если не умер, устанавливаем счетчик времени. If ret = FALSE Then _level.moninfo(midx).effects(meAcidFog).cnt = spl.lvl _level.moninfo(midx).effects(meAcidFog).dam = dam End If txt = "Acid Fog Spell inflicted " & dam & " damage to " & _level.moninfo(midx).mname & " for " & spl.lvl & " turns." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splFireCloak 'КнигаЗакл.: Цель получает 10 повреждений lvl ходов ret = ApplyDamage(mx, my, dam) 'Если не умер, устанавливаем счетчик времени. If ret = FALSE Then _level.moninfo(midx).effects(meFire).cnt = spl.lvl _level.moninfo(midx).effects(meFire).dam = dam End If txt = "Fire Cloak Spell inflicted " & dam & " damage to " & _level.moninfo(midx).mname & " for " & spl.lvl & " turns." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splBlind 'КнигаЗакл.: Ослепляет цель на lvl ходов txt = _level.moninfo(midx).mname & " is blinded for " & spl.lvl & " turns." _level.moninfo(midx).effects(meBlind).cnt = spl.lvl _level.moninfo(midx).effects(meBlind).dam = dam PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splFear 'КнигаЗакл.: Обращает монстров в бегство на lvl ходов. txt = _level.moninfo(midx).mname & " is filled with fear for " & spl.lvl & " turns." _level.moninfo(midx).effects(meFear).cnt = spl.lvl _level.moninfo(midx).effects(meFear).dam = dam PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splConfuse 'КнигаЗакл.: Оглушает монстра на lvl ходов. txt = _level.moninfo(midx).mname & " is confused for " & spl.lvl & " turns." _level.moninfo(midx).effects(meConfuse).cnt = spl.lvl _level.moninfo(midx).effects(meConfuse).dam = spl.lvl PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splFireBomb, splFireBall 'КнигаЗакл.: Повреждение по площади 20 (или 10) * lvl. Поджигает монстра на lvl ходов. 'Проверим, возможно монстр уже в огне (предотвращает бесконечный цикл). If _level.moninfo(midx).effects(meFire).cnt < 1 Then ret = ApplyDamage(mx, my, dam * spl.lvl) 'Если не умер, устанавливаем счетчик времени. If ret = FALSE Then _level.moninfo(midx).effects(meFire).cnt = spl.lvl _level.moninfo(midx).effects(meFire).dam = dam End If 'Проверим тип заклинания. If spl.id = splFireBomb Then txt = "Fire Bomb Spell inflicted " & dam & " damage to " & _level.moninfo(midx).mname & "." Else txt = "Fire Ball Spell inflicted " & dam & " damage to " & _level.moninfo(midx).mname & "." End If PrintMessage txt 'Будем вызывать рекурсивно, чтобы нанести повреждения рядом 'стоящим монстрам. Это вызовет цепную реакцию, повреждая всех 'монстров, которые стоят рядом друг с другом. For i As compass = north To nwest 'Установим начальную позицию. vm.vx = mx vm.vy = my 'Получим новую позицию. vm += i 'Рекурсивно вызовем функцию. tmp = ApplySpell(spl, vm.vx, vm.vy) Next Else txt = _level.moninfo(midx).mname & " is already on fire." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta End If Case splEntangle 'КнигаЗакл.: Обездвиживает монстра на lvel ходов и наносит lvl повреждений каждый ход. ret = ApplyDamage(mx, my, dam) 'Если не умер, устанавливаем счетчик времени. If ret = FALSE Then _level.moninfo(midx).effects(meEntangle).cnt = spl.lvl _level.moninfo(midx).effects(meEntangle).dam = dam End If txt = "Entangle Spell inflicted " & dam & " damage to " & _level.moninfo(midx).mname & " for " & spl.lvl & " turns." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splCloudMind 'КнигаЗакл.: Цель не может пользоваться магией lvl ходов. txt = _level.moninfo(midx).mname & "mind is clouded for " & spl.lvl & " turns." _level.moninfo(midx).effects(meEntangle).cnt = spl.lvl _level.moninfo(midx).effects(meEntangle).dam = dam PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splIceStatue 'КнигаЗакл.: Замораживает цель на lvl ходов. Если цель заморожена, то может быть уничтожена с одного удара. _level.moninfo(midx).effects(meIceStatue).cnt = spl.lvl _level.moninfo(midx).effects(meIceStatue).dam = dam txt = _level.moninfo(midx).mname & " is frozen for " & spl.lvl & " turns." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splRust 'КнигаЗакл.: Уменьшает броню на lvl * 10%. pct = spl.lvl * .10 _level.moninfo(midx).armval = _level.moninfo(midx).armval - pct If _level.moninfo(midx).armval < 0.0 Then _level.moninfo(midx).armval = 0.0 Endif txt = _level.moninfo(midx).mname & " armor has been reduced." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splShatter 'КнигаЗакл.: Уничтожить оружие цели, если возможно. ret = ApplyDamage(mx, my, spl.lvl) 'Если не умер, устанавливаем счетчик времени. If ret = FALSE Then _level.moninfo(midx).atkdam = 0 End If txt = "Shatter Spell destroyed " & _level.moninfo(midx).mname & " attack ability." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splMagicDrain 'КнигаЗакл.: Уменьшает MDF цели на lvl% и добавляет заклинателю на 1 ход. _level.moninfo(midx).effects(meMDF).cnt = spl.lvl _level.moninfo(midx).effects(meMDF).dam = dam txt = "Magic Drain Spell has lowered " & _level.moninfo(midx).mname & " magic defense." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splPoison 'КнигаЗакл.: Отравляет цель по 1 HP на lvl ходов. ret = ApplyDamage(mx, my, dam) 'Если не умер, устанавливаем счетчик времени. If ret = FALSE Then _level.moninfo(midx).effects(mePoison).cnt = spl.lvl _level.moninfo(midx).effects(mePoison).dam = dam Endif txt = "Poison Spell has poisoned " & _level.moninfo(midx).mname & " for " & spl.lvl & " turns." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splEnfeeble 'КнигаЗакл.: Уменьшает боевые факторы цели на lvl * 10%. _level.moninfo(midx).effects(meEnfeeble).cnt = spl.lvl _level.moninfo(midx).effects(meEnfeeble).dam = dam txt = "Enfeeble Spell has lowered " & _level.moninfo(midx).mname & " combat factors for " & spl.lvl & " turns." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splShout 'КнигаЗакл.: Оглушает всех видимых монстров на lvl ходов. For i As Integer = 1 To _level.nummon If _level.moninfo(i).isdead = FALSE Then If _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).visible = TRUE Then _level.moninfo(i).effects(meStun).cnt = spl.lvl _level.moninfo(i).effects(meStun).dam = dam txt = "Warrior Shout Spell has stunned " & _level.moninfo(i).mname & " for " & spl.lvl & " turns." PrintMessage txt Endif End If Next _level.moninfo(midx).mcolor = fbMagenta Case splMindBlast 'КнигаЗакл.: Уменьшает MCF и MDF на lvl% на lvl ходов. _level.moninfo(midx).effects(meMDF).cnt = spl.lvl _level.moninfo(midx).effects(meMDF).dam = dam _level.moninfo(midx).effects(meMCF).cnt = spl.lvl _level.moninfo(midx).effects(meMCF).dam = dam txt = "Mind Blast Spell has lowered " & _level.moninfo(midx).mname & " magic magic combat factors." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta Case splTeleport 'Нанесем достаточно повреждений, чтобы убить любого монстра. ret = ApplyDamage(mx, my, 1000000) txt = "You tleported into " & _level.moninfo(midx).mname & " killing it." PrintMessage txt End Select
Выше показана реализация применения атакующих заклинаний. Большинство из них нуждается в пояснении. Они работают также как и заклинания предметов и или немедленно влияют на монстра, либо имеют долгосрочный эффект. Для обработки новых долгосрочных эффектов, воздействующих на монстров, мы должны расширить их список.
monster.bi'Описание типа для монстра. 'Эффекты заклинаний, воздействующие на монстров. Enum monSpells mePoison 'Повреждение ядом. meFire 'Повреждение огнем. meStun 'Монстр оглушен. meAcidFog '5 повреждений, продолжительностью lvl ходов meBlind 'Ослепление на lvl ходов meFear 'Обращает монстра в бегство на lvl ходов. meConfuse 'Дезориентировать монстра на lvl ходов. meEntangle 'Обездвижить монстра на level ходов и нанести lvl повреждений каждый ход. meCloudMind 'Уль не может пользоваться магией lvl ходов. meMagicDrain 'Уменьшить MDF цели на lvl% и добавить заклинателю на 1 ход. meEnfeeble 'Уменьшить боевые факторы цеди на lvl * 10%. meIceStatue 'Заморозить цель на level ходов. meMDF 'Уменьшить магическую защиту. meMCF 'Уменьшить магическую атаку. End Enum Type montype ... effects(mePoison To meMCF) As monSpellEffects 'Текущий эффект активирован на монстре. End Type
Как вы можете видеть, мы расширили перечисление monSpells и используем первое и последнее значение в качестве границ массива. Это позволит на ссылаться на элемент массива используя в качестве индекса имя из перечисления. Чтобы посмотреть как это работает, давайте рассмотрим одно из заклинаний, которое имеет долгосрочный эффект.
map.bi'Обрабатывает все временные события. Sub levelobj.DoTimedEvents() Dim As String txt Dim As Integer tmp 'Перебор всех монстров. For i As Integer = 1 To _level.nummon 'Убедимся что монстр не мертв. If _level.moninfo(i).isdead = FALSE Then 'Проверим все эффекты и назначим повреждения/состояния. If _level.moninfo(i).effects(mePoison).cnt > 0 Then tmp = ApplyDamage(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y, _level.moninfo(i).effects(mePoison).dam) _level.moninfo(i).effects(mePoison).cnt -= 1 Else _level.moninfo(i).mcolor = fbRedBright Endif ...
Это код для состояния отравления. Каждый ход монстру наносятся повреждения и уменьшается счетчик ходов. Как только счетчик достигает нуля — эффект больше не применяется. Это чрезвычайно простой и эффективный метод реализации временных эффектов для пошаговой игры.
Что произойдет, если монстр умрет от отравления? Мы можем это увидеть в функции ApplyDamage.
map.bi'Назначает повреждения монстрам. Возвращает true если монстр умирает. 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 Dim As String txt 'Убодимся что монстр здесь. 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 < 1) Or (_level.moninfo(midx).effects(meIceStatue).cnt > 0) Then pchar.CurrXP = pchar.CurrXP + _level.moninfo(midx).xp 'Монстр мертв. 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 'Отобразим результат боя. If _level.moninfo(midx).isdead = TRUE Then txt = pchar.CharName & " killed the " & _level.moninfo(midx).mname & " with " & dam & " damage points." Else txt = pchar.CharName & " hit the " & _level.moninfo(midx).mname & " for " & dam & " damage points." Endif PrintMessage txt Endif Return ret End Function
Если монстр умирает, то мы добавляем персонажу опыт (pchar.CurrXP = pchar.CurrXP + _level.moninfo(midx).xp) и выбрасываем на землю предметы из инвентаря монстра. Мы вызываем эту функцию не только из ApplySpell и DoTimedEvents, но и в процедурах ближнего и дистанционного боя в dod.bas. Снова повторюсь: повторное использование кода — наш друг.
Есть два заклинания в ApplySpell, которые требуют детального рассмотрения.
map.biCase splFireBomb, splFireBall 'КнигаЗакл.: Повреждение по площади 20 (или 10) * lvl. Поджигает монстра на lvl ходов. 'Убедимся, что монстр уже не горит (для исключения бесконечного цикла). If _level.moninfo(midx).effects(meFire).cnt < 1 Then ret = ApplyDamage(mx, my, dam * spl.lvl) 'Если не умер, установим счетчик времени (ходов). If ret = FALSE Then _level.moninfo(midx).effects(meFire).cnt = spl.lvl _level.moninfo(midx).effects(meFire).dam = dam End If 'Проверим тип заклинания. If spl.id = splFireBomb Then txt = "Fire Bomb Spell inflicted " & dam & " damage to " & _level.moninfo(midx).mname & "." Else txt = "Fire Ball Spell inflicted " & dam & " damage to " & _level.moninfo(midx).mname & "." End If PrintMessage txt 'Будем вызывать рекурсивно, чтобы нанести повреждения рядом 'стоящим монстрам. Это вызовет цепную реакцию, повреждая всех 'монстров, которые стоят рядом друг с другом. For i As compass = north To nwest 'Установим начальную позицию. vm.vx = mx vm.vy = my 'Получим новую позицию. vm += i 'Рекурсивно вызовем функцию снова. tmp = ApplySpell(spl, vm.vx, vm.vy) Next Else txt = _level.moninfo(midx).mname & " is already on fire." PrintMessage txt _level.moninfo(midx).mcolor = fbMagenta End If
Тут у нас обрабатываются заклинания «Огненный Шар» и «Огненная Бомба». Так как оба заклинания ведут себя одинаково, то и реализацию этих заклинаний мы можем описать в одном месте. В отличии от других заклинаний, которые воздействуют только на одного монстра, эти заклинания наносят дополнительный ущерб. Если два монстра стоят рядом, то будут поражены оба монстра. Мы добиваемся этого рекурсивным вызовом функции ApplySpell.
Рекурсия — мощное средство, и позволяет упростить многие задачи в программировании. Здесь мы проверяем каждую клетку карты вокруг цели заклинания, и применяем его эффект на эти клетки. Если на одной из этих клеток окажется монстр, то он будет атакован заклинанием, как будто он был целью. Затем мы рассмотрим каждый квадрат вокруг нового монстра и так далее. При использовании рекурсии, вы должны убедиться, что у вас есть условие выхода из нее, иначе программа будет снова и снова вызывать одну и туже функцию из самой себя, пока стек не переполниться и программа не «упадет». У нас имеется два условия выхода: если монстра нет на проверяемой ячейке карты, и если монстр уже подожжен. Последнее условие наиболее важно. Если мы будем все время поджигать уже горящих монстров, то цикл никогда не завершиться, что равносильно сбою программы. Проверка обоих условий обеспечит нам безопасный выход из рекурсии.
Это было атакующее заклинание, давайте теперь рассмотрим заклинание, которое изменяет параметры персонажа.
dod.bas:CastSpellSelect Case sinv.spell.id Case splHeal tmp = pchar.MaxHP * (sinv.spell.lvl / 100) If tmp < 1 Then tmp = 1 pchar.CurrHP = pchar.CurrHP + tmp Case splMana tmp = pchar.MaxMana * (sinv.spell.lvl / 100) If tmp < 1 Then tmp = 1 pchar.CurrMana = pchar.CurrMana + tmp cancel = TRUE
Восстановление здоровья и восстановление маны делают именно то, что написано у них в названии — восстанавливают здоровье и ману соответственно. Разница в том, что для заклинания восстановления маны не требуется мана. Мы добиваемся этого, используя флаг cancel. Когда мы доберемся до расчета потраченной маны, вы увидите как этот флаг работает.
Заклинание перезарядки жезлов несколько сложнее, но только потому, что мы должны проверить инвентарь и слоты экипировки на наличие этих самых жезлов.
dod.bas:CastSpellCase splRecharge 'Поищем жезлы в инвентаря и перезарядим в соответствии с уровнем. For i = pchar.LowInv To pchar.HighInv iitem = pchar.HasInvItem(i) If iitem = TRUE Then 'Получим предмет инвентаря. pchar.GetInventoryItem i, iinv 'Проверим что это оружие. If iinv.classid = clWeapon Then 'Убедимся что это жезл. If iinv.weapon.iswand = TRUE Then 'Увеличим кол-во зарядов. iinv.weapon.ammocnt += sinv.spell.lvl 'Убедимся, что не превысили максимальный уровень зарядов. If iinv.weapon.ammocnt > iinv.weapon.capacity Then iinv.weapon.ammocnt = iinv.weapon.capacity Endif 'Вернем предмет в инвентарь. pchar.AddInvItem iitem, iinv Endif Endif End If Next 'Проверим главный слот экипировки оружия. iitem = pchar.HasInvItem(wPrimary) If iitem = TRUE Then pchar.GetInventoryItem wPrimary, iinv If iinv.classid = clWeapon Then 'Убедимся что это жезл. If iinv.weapon.iswand = TRUE Then iinv.weapon.ammocnt += sinv.spell.lvl If iinv.weapon.ammocnt > iinv.weapon.capacity Then iinv.weapon.ammocnt = iinv.weapon.capacity Endif pchar.AddInvItem wPrimary, iinv Endif Endif Endif 'Проверим второй слот экипировки оружия. iitem = pchar.HasInvItem(wSecondary) If iitem = TRUE Then pchar.GetInventoryItem wSecondary, iinv If iinv.classid = clWeapon Then 'Убедимся что это жезл. If iinv.weapon.iswand = TRUE Then iinv.weapon.ammocnt += sinv.spell.lvl If iinv.weapon.ammocnt > iinv.weapon.capacity Then iinv.weapon.ammocnt = iinv.weapon.capacity Endif pchar.AddInvItem wSecondary, iinv Endif Endif Endif
В первом For-Next цикле мы проверяем все предметы инвентаря, чтобы найти жезлы. Если жезл найден, то мы увеличиваем количество его зарядов в соответствии с уровнем заклинания. Так как жезл нельзя зарядить большим количеством зарядов чем максимально возможное для данного жезла, то мы должны проверить максимальный уровень и уменьшить кол-во зарядов, если оно превышает максимально допустимое значение. Заряженный жезл мы возвращаем в тот же слот инвентаря.
Следующие два фрагмента кода делают тоже самое, но вместо инвентаря, проверяют первичный и вторичный слот экипировки персонажа, так как жезлы могут быть как персонаж может держать жезлы как в правой, так и в левой руке (или обоих).
dod.bas:CastSpellCase splFocus 'Увеличивает все боевые факторы на 1 ход. pchar.BonUcf = sinv.spell.lvl pchar.BonUcfCnt = 1 pchar.BonAcf = sinv.spell.lvl pchar.BonAcfCnt = 1 pchar.BonPcf = sinv.spell.lvl pchar.BonPcfCnt = 1 pchar.BonCdf = sinv.spell.lvl pchar.BonCdfCnt = 1 pchar.BonMcf = sinv.spell.lvl pchar.BonMcfCnt = 1 pchar.BonMdf = sinv.spell.lvl pchar.BonMdfCnt = 1
Заклинание фокусировки увеличивает все боевые факторы персонажа на 1 ход. Поэтому для всех боевых факторов мы устанавливаем бонус в соответствии с уровнем заклинания и для каждого бонуса выставляем счетчик ходов на 1. Помните, что мы решили не накапливать бонусы факторов в игре, поэтому, если на момент использования заклинания на персонажа уже действовал какой либо из затрагиваемых бонусов, то его значение перезапишется.
dod.bas:CastSpellCase splTeleport 'Получим координаты цели. ret = GetTargetCoord(vt, sinv.spell.lvl) If ret = TRUE Then 'Проверим, возможно в координатах находится монстр. If level.IsMonster(vt.vx, vt.vy) = TRUE Then 'Телепорт в монстра убивает его. ret = level.ApplySpell(sinv.spell, vt.vx, vt.vy) 'Установим новую позицию персонажа. pchar.Locx = vt.vx pchar.Locy = vt.vy 'Сгенерируем карту звука. level.ClearSoundMap snd = pchar.GetNoise() level.GenSoundMap(pchar.Locx, pchar.Locy, snd) Else 'Убедимся, что позиция не заблокирована. If level.IsBlocking(vt.vx, vt.vy) = FALSE Then 'Установим новую позицию персонажа. pchar.Locx = vt.vx pchar.Locy = vt.vy 'Сгенерируем карту звука. level.ClearSoundMap snd = pchar.GetNoise() level.GenSoundMap(pchar.Locx, pchar.Locy, snd) Else ShowMsg "Teleport", "You can't teleport there.", tWidgets.MsgBoxType.gmbOK cancel = TRUE End If Endif Else cancel = TRUE Endif
Заклинание телепортации позволяет выбрать место, куда персонаж должен перенестись. Для указания координат места, мы вызываем функцию GetTargetCoord. Обратите внимание, что если персонаж телепортируется в монстра, то это должно убить его, поэтому мы вызываем тут ApplySpell, хотя, технически, телепортация это не заклинание атаки.
Как только игрок выбирает верное место для телепортации (ячейка пола а не стену), мы перемещаем его в данную локацию, также, как будто он туда пришел пешком. Дополнительно мы генерируем необходимые данные, например карту звука. Также, мы используем здесь флаг cancel для отмены расхода маны, если игрок решил не телепортироваться, уже выбрав заклинание телепортации.
Следующее заклинание, которое мы рассмотрим, это заклинание отпирания дверей.
dod.bas:CastSpellCase splOpen 'Получим координаты цели. ret = GetTargetCoord(vt, sinv.spell.lvl) If ret = TRUE Then 'Получим идентификатор местности. tid = level.GetTileID(vt.vx, vt.vy) 'Проверим, что это закрытая дверь. If tid = tDoorClosed Then 'Убедимся что она заперта. If level.IsDoorLocked(vt.vx, vt.vy) = TRUE Then ret = level.OpenLockedDoor(vt.vx, vt.vy, sinv.spell.lvl) If ret = TRUE Then PrintMessage "Door was opened." Endif Else 'Сообщим игроку, что дверь не заперта. ShowMsg "Open Spell", "The door is not locked.", tWidgets.MsgBoxType.gmbOK cancel = TRUE Endif Endif Else cancel = TRUE End If
Заклинание отпирания дверей пытается отпереть запертую дверь, от уровня заклинания увеличивается вероятность успеха. Мы снова вызываем функцию GetTargetCoord чтобы игрок указал координаты двери, которую он хочет попробовать отпереть, после чего мы передаем управление функции OpenLockedDoor, которую мы добавили в объект уровня подземелья.
level.bi'Попытаемся открыть запертую дверь. Function levelobj.OpenLockedDoor(x As Integer, y As Integer, dr As Integer) As Integer Dim As Integer ret = TRUE, ddr, rolld, rollp Dim tid As terrainids 'Убедимся что по указанным координатам есть дверь и она заперта. tid = GetTileID(x, y) If tid = tDoorClosed Then If IsDoorLocked(x, y) = TRUE Then 'Получим рейтинг сложности двери. ddr = _level.lmap(x, y).doorinfo.lockdr 'Получим случайные значения. rollp = RandomRange(1, dr) rolld = RandomRange(1, ddr) If rollp > rolld Then 'Откроем дверь. _level.lmap(x, y).doorinfo.locked = FALSE SetTile x, y, tdooropen Else 'Попытка провалилась. ret = FALSE Endif Endif Endif Return ret End Function
Здесь мы получаем два случайных числа, зависящих от сложности двери и мастерства вскрытия замков (передаваемая в функцию переменная dr). Сложность двери представляет собой сложность замка и количество усилий, которые необходимо приложить для его вскрытия. Если случайное число, зависящее от мастерства вскрытия замков больше случайного числа зависящего от сложности замка — дверь отпирается. Эту же функцию, мы будем использовать не только для заклинания, но и когда персонаж собственноручно будет пытаться взломать замок. Сейчас у нас нет запертых дверей, но в скором времени мы их добавим и уже сейчас можен подготовиться к их взлому.
Последнее заклинание, действующее на персонажа, это «случайная телепортация».
dod.bas:CastSpellCase splBlink 'Установим эффект заклинания мерцания. pchar.SetSpellEffect sinv.spell.id, sinv.spell.lvl, 0
Изначально, заклинание мерцания, это должна была быть случайная телепортация, однако, поскольку у нас уже есть заклинания телепортации, я решил поступить с мерцанием как то иначе. Обратите внимание, мы просто вызываем SetSpellEffect передавая в него идентификатор нашего заклинания.
character.bi'Установим эффект заклинания. Sub character.SetSpellEffect(splid As cspleffects, scnt As Integer, samt As Integer) Select Case splid Case cPoison _cinfo.cseffect(cPoison).cnt = scnt _cinfo.cseffect(cPoison).amt = samt Case cBlink _cinfo.cseffect(cBlink).cnt = scnt _cinfo.cseffect(cBlink).amt = samt End Select End Sub
Как вы видите, мы добавили новый эффект в массив эффектов персонажа. Ранее, у нас, за эффект отравления отвечал просто флаг и счетчик ходов, но, на самом деле, нам необходимо добавить массив эффектов, воздействующих на персонажа, как мы сделали это для монстров, поэтому мы отбросили флаг отравления и добавили отравление персонажа, как один из эффектов, которые на него воздействуют.
character.bi'Эффекты заклинаний. Enum cspleffects cPoison cBlink End Enum 'Описание типа эффекта. Type cspleftype cnt As Integer 'Продолжительность. amt As Integer 'Мощность эффекта. End Type 'Определение типа атрибутов персонажа. Type characterinfo ... cseffect(cPoison To cBlink) As cspleftype 'Массив эффектов заклинаний. End Type
Мы добавили массив эффектов, воздействующих на персонажа, по аналогии с тем, как мы сделали раньше для монстров. У нас есть мощность и продолжительность эффекта, которыми мы будем управлять в подпрограмме DoTimedEvents.
character.bi'Управление всеми продолжительными эффектами. Sub character.DoTimedEvents() Dim As Integer roll1, roll2, v1, v2, amt, statamt 'Яд наносит повреждения персонажу, в зависимости от силы отравления. If Poisoned = TRUE Then 'Получим силу яда. v1 = PoisonStr 'Получим выносливость персонажа + бонус v2 = CurrSta + BonSta 'Возьмем случайные значения. roll1 = RandomRange(1, v1) roll2 = RandomRange(1, v2) 'Если яд выиграл, If roll1 > roll2 Then 'Отнимем единицу здоровья. CurrHP = CurrHP - 1 Endif Endif 'Проверим счетчики бонусов и применим необходимые бонусы. 'Сила. If BonStrCnt > 0 Then BonStrCnt = BonStrCnt - 1 If BonStrCnt < 1 Then BonStr = 0 Endif Endif 'Выносливость If BonStaCnt > 0 Then BonStaCnt = BonStaCnt - 1 If BonStaCnt < 1 Then BonSta = 0 Endif Endif 'Ловкость. If BonDexCnt > 0 Then BonDexCnt = BonDexCnt - 1 If BonDexCnt < 1 Then BonDex = 0 Endif Endif 'Подвижность If BonAglCnt > 0 Then BonAglCnt = BonAglCnt - 1 If BonAglCnt < 1 Then BonAgl = 0 Endif Endif 'Интеллект. If BonIntCnt > 0 Then BonIntCnt = BonIntCnt - 1 If BonIntCnt < 1 Then BonInt = 0 Endif charint = _cinfo.intatt(idxAttr) + BonInt Endif 'Безоружный бой. If BonUcfCnt > 0 Then BonUcfCnt = BonUcfCnt - 1 If BonUcfCnt < 1 Then BonUcf = 0 Endif Endif 'Ближний Бой с оружием. If BonAcfCnt > 0 Then BonAcfCnt = BonAcfCnt - 1 If BonAcfCnt < 1 Then BonAcf = 0 Endif Endif 'Дистанционный бой. If BonPcfCnt > 0 Then BonPcfCnt = BonPcfCnt - 1 If BonPcfCnt < 1 Then BonPcf = 0 Endif Endif 'Магический бой. If BonMcfCnt > 0 Then BonMcfCnt = BonMcfCnt - 1 If BonMcfCnt < 1 Then BonMcf = 0 Endif Endif 'Защита. If BonCdfCnt > 0 Then BonCdfCnt = BonCdfCnt - 1 If BonCdfCnt < 1 Then BonCdf = 0 Endif Endif 'Магическая защита. If BonMdfCnt > 0 Then BonMdfCnt = BonMdfCnt - 1 If BonMdfCnt < 1 Then BonMdf = 0 Endif Endif 'Проверим ожерелья и кольца. statamt = MaxHP amt = GetJewleryEffect(jwRegenHP, statamt) CurrHP = CurrHP + amt statamt = MaxMana amt = GetJewleryEffect(jwRegenMana, statamt) CurrMana = CurrMana + amt 'Проверим заклинание мерцания. If _cinfo.cseffect(cBlink).cnt > 0 Then _cinfo.cseffect(cBlink).cnt = _cinfo.cseffect(cBlink).cnt - 1 If _cinfo.cseffect(cBlink).cnt < 0 Then _cinfo.cseffect(cBlink).cnt = 0 Endif End Sub
Здесь вы можете видеть два изменения. Первое — для эффекта отравление теперь используется массив состояний, а также, в последней части кода, добавлена проверка и уменьшение счетчика для эффекта случайного телепорта. Но где же сама реализация этого эффекта? Давайте взглянем на код атаки монстров.
level.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) And (pchar.BlinkActive = FALSE) Then ...
Если флаг заклинания мерцания активен, то персонаж «невидим» для монстра в этом раунде, и монстр не может его атаковать. Свойство объекта персонажа BlinkActive возвращает текущее состояние флага заклинания мерцания.
character.bi'Возврашает TRUE если заклинание мерцания активно. Property character.BlinkActive() As Integer Return (_cinfo.cseffect(cBlink).cnt > 0) End Property
Предыдущий код может выглядеть несколько странно, если вы не видели этой техники раньше. Мы при помощи оператора > проверяем количество оставшихся ходов для действия заклинания мерцания. Так как оператор > это внутренняя функция, которая возвращает True (-1) или False (0), то мы можем воспользоваться этим для получения нашего возвращаемого значения. Это быстрый и эффективный метод для проверки значения переменной.
Оставшаяся часть кода в CastSpell следит за вычитанием необходимого количества маны после использования заклинания.
dod.bas:CastSpell... End Select Endif If cancel = FALSE Then 'Отнимем необходимое кол-во маны. pchar.CurrMana = pchar.CurrMana - sinv.spell.manacost End If ...
Здесь мы видим как работает флаг cancel, который отвечает за то, необходимо ли отнимать ману у персонажа. Если флаг не установлен, то мы вычитаем необходимое количества маны, из текущего ее количества у персонажа.
Мы рассмотрели почти весь код для реализации заклинаний, однако, существуют некоторые заклинания, например Заморозки, или Дезориентации, которые влияют на передвижение монстров, поэтому му должны внести изменения в код процедуры MoveMonsters.
level.bi'Перемещение всех дивых монстров. Sub levelobj.MoveMonsters () Dim As mcoord nxt Dim As Integer pdist 'Переберем всех монстров. For i As Integer = 1 To _level.nummon 'Убедимся, что монстр жив. If (_level.moninfo(i).isdead = FALSE) And _ (_level.moninfo(i).effects(meStun).cnt < 1) And _ (_level.moninfo(i).effects(meBlind).cnt < 1) And _ (_level.moninfo(i).effects(meEntangle).cnt < 1) And _ (_level.moninfo(i).effects(meIceStatue).cnt < 1) And _ (_level.moninfo(i).effects(meConfuse).cnt < 1) Then 'Монстр убегает? If _level.moninfo(i).flee = FALSE Then ...
Тут мы проверяем все заклинания, которые влияют на движение монстров. Некоторые из них, наносят монстрам урон, как например, заклинание «Спутать». Это дает персонажу нанести дополнительные «бесплатные» повреждения монстрам, что делает эти заклинания весьма ценными. Заклинание «Ледяная Статуя» также дает дополнительную возможность убить монстра с одного удара, когда он под его воздействием.
level.bi'Наносит повреждения монстрам, возвращает true если монстр умирает. 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 Dim As String txt 'Убедимся что монстр здесь. 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 < 1) Or (_level.moninfo(midx).effects(meIceStatue).cnt > 0) Then pchar.CurrXP = pchar.CurrXP + _level.moninfo(midx).xp 'Монстр умер. ret = TRUE 'Установим флаг, указывающий что монстр мертв. _level.moninfo(midx).isdead = TRUE ...
Здесь мы видим, что если заклинание «Ледяная Статуя» активно, то монстр, при получении повреждений, сразу же умирает. Размещая код заклинания здесь, нам не нужно беспокоится о процедурах боя. Если, в какой то момент, мы решим добавить новые боевые режимы, то заклинание будет охватывать и их тоже.
Как вы можете видеть, реализация применения заклинаний — нетривиальная задача. Но она стоит того, чтобы потратить на нее время. Это даст игроку набор новых опций, расширяя его возможности по активному участию в игровом процессе, в результате чего игра будет поддерживать у игрока интерес к себе.
Перевод на русский: Fantik
содержание | назад | вперед