Теперь,когда у нас есть изученные заклинания, перейдем к их использованию персонажем. Чтобы вызвать какое либо заклинание, игрок использует клавишу «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
...Здесь мы видим, что если заклинание «Ледяная Статуя» активно, то монстр, при получении повреждений, сразу же умирает. Размещая код заклинания здесь, нам не нужно беспокоится о процедурах боя. Если, в какой то момент, мы решим добавить новые боевые режимы, то заклинание будет охватывать и их тоже.
Как вы можете видеть, реализация применения заклинаний — нетривиальная задача. Но она стоит того, чтобы потратить на нее время. Это даст игроку набор новых опций, расширяя его возможности по активному участию в игровом процессе, в результате чего игра будет поддерживать у игрока интерес к себе.

Комментариев нет:
Отправить комментарий