Давайте сделаем рогалик (Инвентарь персонажа)

charinv.png

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

character.bi

'Определение типа данных характеристик персонажа.
Type characterinfo
    cname As String * 35 'Имя персонажа.
    stratt(3) As Integer 'Сила (0), бонус силы (1), продолжительность действия бонуса в ходах (2)
    staatt(3) As Integer 'Выносливость
    dexatt(3) As Integer 'Ловкость
    aglatt(3) As Integer 'Проворство
    intatt(3) As Integer 'Интеллект
    currhp As Integer    'Текущее здоровье
    maxhp As Integer     'Максимальное здоровье
    currmana As Integer  'Текущая мана
    maxmana As Integer   'Максимальная мана
    ucfsk(3) As Integer  'Рукопашный бой
    acfsk(3) As Integer  'Оружие ближнего боя
    pcfsk(3) As Integer  'Дистанционное оружие
    mcfsk(3) As Integer  'Магическая атака
    cdfsk(3) As Integer  'Защита
    mdfsk(3) As Integer  'Магическая защита
    currxp As Integer    'Текущий, расходуемый опыт.
    totxp As Integer     'Общая сумма опыта, за время жизни персонажа.
    currgold As Integer  'Текущее количество золота.
    totgold As Integer   'Общая сумма золота за время жизни персонажа.
    ploc As mcoord       'Ткущие x и y координаты персонажа.
    cinv(97 To 122) As invtype 'Инвентарь персонажа для индексации использует значения asii кодов.
End Type


Последнее определение в описании персонажа, это наша коллекция предметов инвентаря. Обратите внимание на странный диапазон индексов. Это ASCII коды символов, заканчивающиеся буквой z. Персонаж будет иметь 26 слотов для предметов инвентаря, в дополнение к тем предмета, которые на нем одеты и которые он держит в руках. Так как индекс массива инвентаря у нас является кодом символов, то для выбора какого либо предмета нам необходимо только получить код символа клавиши клавиатуры, которую нажал пользователь. Здесь мы воспользуемся гибкостью языка FreeBasic и большую часть работы по выбору предметов из инвентаря за нас выполнить компилятор. Многие программисты не пользуются гибкостью языка, и, в конечном итоге, получают громоздкий, трудно поддерживаемый, код.

Что нам нужно сделать сейчас, это позволить игроку поднять предмет, находящийся на карте. Для этого мы добавляем действие «Поднять», если игрок нажимает клавишу «g».

dod.bas

'Поднять предмет с пола и положить в инвентарь персонажа.
If ckey = "g" Then
    'Inventory type.
    Dim inv As invtype
    'Если под персонажем есть предмет.
    If level.HasItem(pchar.Locx, pchar.Locy) = TRUE Then
        'проверить на залото. Добавляем золото и сколько всего было золота.
        Dim iclass As classids = level.GetInvClassID(pchar.Locx, pchar.Locy)
        If iclass = clGold Then
            'Поднимаем золото с карты.
            level.GetItemFromMap pchar.Locx, pchar.Locy, inv
            'Добавим к золоту персонажа.
            pchar.CurrGold = pchar.CurrGold + inv.gold.amt
            pchar.TotGold = pchar.TotGold + inv.gold.amt
            'Добавим опыт.
            pchar.CurrXP = pchar.CurrXP + inv.gold.amt
            pchar.TotXP = pchar.TotXP + inv.gold.amt
            'Выведем сообщение для игрока.
            PrintMessage inv.gold.amt & " gold coins collected."
            DrawMainScreen
        Else
            'Проверим, есть ли свободный слот в инвентаре.
            Dim As Integer cidx = pchar.GetFreeInventoryIndex
            'Если слот найден, поместим туда предмет.
            If cidx <> -1 Then
                level.GetItemFromMap pchar.Locx, pchar.Locy, inv
                'Добавим в инвентарь персонажа.
                pchar.AddInvItem cidx, inv
                PrintMessage "Item added to inventory."
            Else
                'Нет свободных слотов.
                PrintMessage "No free inventory slots."
            Endif
        Endif
    Else
        PrintMessage "Nothing to get."
    Endif
Endif


Когда игрок нажимает клавишу «g», мы проверяем, вначале, стоит ли персонаж на предмете. Если предмет присутствует на той же ячейке карты, что и персонаж, то, далее, мы получаем идентификатор класса этого предмета. Это нам нужно для того, что бы отличить золото от других предметов. Золото не добавляется в инвентарь персонажа а изменяет значение переменно общего количество денег персонажа, а также, в качестве бонуса, добавляет некоторое количество опыта. Если предмет не золото, то мы добавляем его в инвентарь.

При добавлении предмета в инвентарь нам нужно проверить, если ли в нем свободное место. Мы делаем это в функции GetFreeInventoryIndex.

character.bi

'Возвращает индекс свободного слота инвентаря или -1.
Function character.GetFreeInventoryIndex() As Integer
    Dim As Integer ret = -1

    'Ишем пустой слот.
    For i As Integer = Lbound(_cinfo.cinv) To Ubound(_cinfo.cinv)
        'Прверяем идентификатор класса.
        If _cinfo.cinv(i).classid = clNone Then
            'Пустой слот.
            ret = i
            Exit For
        Endif
    Next

    Return ret
End Function


Эта функция перебирает массив предметов инвентаря и ищет clNone записанное в полн ClassID предмета, что говорит о том, что данная ячейка инвентаря пуста. Если функция находит свободный слот, то она возвращает его индекс, или -1, если все слоты заняты. Как только мы получили индекс свободного слота в инвентаре, мы можем добавить в него предмет, используя процедуру AddInvItem.

character.bi

'Добавляет предмет в слот инвентаря персонажа.
Sub character.AddInvItem(idx As Integer, inv As invtype)
    'Проверим правильность индекса массива.
    If idx >= Lbound(_cinfo.cinv) And idx <= Ubound(_cinfo.cinv) Then
        'Очистим слот инвентаря.
        ClearInv _cinfo.cinv(idx)
        'Добавим предмет.
        _cinfo.cinv(idx) = inv
    End If
End Sub


Мы проверяем, чтобы убедиться, что индекс валиден (так как данная процедура может быть вызвана из других мест программы), очистим текущий слот инвентаря (опять же, в случае, если процедура вызывается из другого места) и установим в слот инвентаря предмет, который игрок взял с карты. Чтобы сообщить игроку о том, что предмет успешно подобран, мы добавляем вывод сообщения об этом в основной код обработки команды «Поднять». Теперь, когда предмет находится в инвентаре персонажа, нам нужно иметь возможность его отобразить, а также другие предметы, которые персонаж подобрал ранее. Это подводит нас к экрану инвентаря персонажа.

dod.bas

'Нарисуем экран инвентаря.
If ckey = "i" Then
    ManageInventory
    'Необходимо перерисовать фон.
    DrawBackground mainback()
    DrawMainScreen
Endif


Когда игрок наживает клавишу «i», мы вызываем подпрограмму ManageInventory, которая обрабатывает команды инвентаря. Как только игрок выходит из инвентаря, основной экран перерисовывается. Итак, все действия над инвентарем персонажа происходят в ManageInventory.

dod.bas

'Управление инвентарем персонажа.
Sub ManageInventory()
    Dim As String kch, ich
    Dim As Integer ret

    DrawInventoryScreen
    Do
        kch = Inkey
        kch = Ucase(kch)
        'Проверим, нажата ли какая либо клавиша.
        If kch <> "" Then
            'Обработка команды опознания предмета.
            If kch = "E" Then
                ret = ProcessEval()
                'Экран изменен.
                If ret = TRUE Then
                    DrawInventoryScreen
                Endif
            Endif
        Endif
        Sleep 1
    Loop Until kch = key_esc
    ClearKeys
End Sub


Вначале мы рисуем экран инвентаря персонажа, на котором и будут обрабатываться все команды работы с инвентарем. На данный момент активна только одна команда «оценить», которая вызывается при нажатии клавиши «e». После того, как игрок сделал все что ему нужно было сделать с инвентарем, мы возвращаем управление основной программе. Функцию ProcessEval() мы рассмотрим немного позже, а пока, давайте посмотрим на процедуру DrawInventoryScreen.

dod.bas

'Выводит на экран инвентарь персонажа.
Sub DrawInventoryScreen()
    Dim As Integer col, row, iitem, ret, srow, ssrow, cnt, i
    Dim As String txt, txt2, desc
    Dim As invtype inv
    Dim As Uinteger clr

    Screenlock
    'Установим фон для экрана инвентаря.
    DrawBackground leatherback()
    'Добавим заголовок.
    txt = "Current Inventory for " & Trim(pchar.CharName)
    col = CenterX(txt)
    row = 1
    'Выведем заголовок с тенью.
    PutTextShadow txt, row, col, fbYellowBright
    'Добавми текущую экипировку.
    col = 2
    row += 3
    txt = "1 Primary: "
    PutTextShadow txt, row, col, fbWhite
    col = txcols / 2
    txt = "4 Neck: "
    PutTextShadow txt, row, col, fbWhite
    col = 2
    row += 2
    txt = "2 Secondary: "
    PutTextShadow txt, row, col, fbWhite
    col = txcols / 2
    txt = "5 Ring Right: "
    PutTextShadow txt, row, col, fbWhite
    col = 2
    row += 2
    txt = "3 Armor: "
    PutTextShadow txt, row, col, fbWhite
    col = txcols / 2
    txt = "6 Ring Left: "
    PutTextShadow txt, row, col, fbWhite
    'Разделительная черта.
    row += 2
    col = 1
    txt = String(80, Chr(205))
    Mid(txt, 2) = " Equipment  (*) = Not Evaluated "
    txt2 = " Gold: " & pchar.CurrGold & " "
    Mid(txt, 80 - Len(txt2)) = txt2
    PutTextShadow txt, row, col, fbYellowBright
    row += 2
    col = 2
    srow = row
    'Выведем предметы в инвентаре.
    For i = pchar.LowInv To pchar.HighInv
        'Проверим, есть ли предмет в слоте.
        iitem = pchar.HasInvItem(i)
        If iitem = TRUE Then
            'Получим предмет из слота.
            pchar.GetInventoryItem i, inv
            'Проверим, опознан или нет.
            ret = IsEval(inv)
            'Получим описание предмета.
            desc = GetInvItemDesc(inv)
            'Получим цвет предмета.
            clr = inv.iconclr
            'Создадим строку с описанием.
            txt = Chr(i) & " " & desc & " "
            'Если предмет не опознан, то отметить его, чтобы игрок знал, что данный предмет нужно опознать.
            If ret = FALSE Then
                txt &= "(*)"
            Endif
        Else
            txt = Chr(i)
            clr = fbWhite
        Endif
        'Перейдем на другой столбец, если достигли середины списка инвентаря.
        cnt += 1
        If cnt =  14 Then
            col = txcols / 2
            'Сохраним номер последней строки, чтобы потом наисовать разделитель.
            ssrow = row
            row = srow
        Endif
        'Выведем текст.
        PutTextShadow txt, row, col, clr
        row += 2
    Next
    'Выведем разделительную линию.
    row = ssrow + 1
    col = 1
    txt = String(80, Chr(205))
    Mid(txt, 2) = " Spells Learned "
    PutTextShadow txt, row, col, fbYellowBright
    'Нарисуем слоты заклинаний.
    col = 2
    row += 2
    cnt = 0
    srow = row
    For i = 65 To 78
        txt = Chr(i)
        'Перейдем на следующую колонку если достигли середины заклинаний.
        cnt += 1
        If cnt =  8 Then
            col = txcols / 2
            'Сохраним номер последней строки. Чтобы знать, откуда продолжить.
            ssrow = row
            row = srow
        Endif
        'Выведем текст.
        PutTextShadow txt, row, col, clr
        row += 2
    Next
    'Разделитель.
    row = ssrow + 1
    col = 1
    txt = String(80, Chr(205))
    Mid(txt, 2) = " Commands "
    PutTextShadow txt, row, col, fbYellowBright
    'Выведем список команд
    row += 2
    txt = "(D)rop - (E)val - (W)ield/Wear - (R)ead - (E)at/Drink - (I)spect"
    col = CenterX(txt)
    PutTextShadow txt, row, col, fbWhite
    Screenunlock
End Sub


Как вы видите, процедура аналогична процедуре вывода главного экрана. Экран разделен на 3 области. В верхней части отображаются предметы одеты на персонаж, и те, что он держит в руках. Средняя часть, это инвентарь персонажа (представьте себе рюкзак, и вещи лежащие в нем), и, наконец, нижняя секция отображает заклинания, которые выучил персонаж читая книги заклинаний. Единственный раздел, который содержит какую либо информацию на данном этапе разработки, это рюкзак, отображающий предметы из инвентаря персонажа.

dod.bas:DrawInventoryScreen

'Проверим, есть ли предмет в слоте.
iitem = pchar.HasInvItem(i)
If iitem = TRUE Then
    'Получим предмет из слота.
    pchar.GetInventoryItem i, inv
    'Проверим, опознан или нет.
    ret = IsEval(inv)
    'Получим описание предмета.
    desc = GetInvItemDesc(inv)
    'Получим цвет предмета.
    clr = inv.iconclr
    'Создадим строку с описанием.
    txt = Chr(i) & " " & desc & " "
    'Если предмет не опознан, то отметить его, чтобы игрок знал, что данный предмет нужно опознать.
    If ret = FALSE Then
        txt &= "(*)"
    Endif
Else
    txt = Chr(i)
    clr = fbWhite
Endif


При заполнения экрана, мы проверяем, есть ли в слоте инвентаря предмет, используя функцию персонажа HasInvItem, и, если она возвращает «истина», получаем предмет из данного слота инвентаря при помощи функции GetInventoryItem. Далее, необходимо проверить, опознан ли данный предмет, используя функцию IsEval, так как неопознанные предметы мы будем отмечать при их отображении, а также получаем описание предмета при помощи функции GetInvItemDesc. Цвет, используемый для вывода описания предмета, соответствует цвету символа предмета при отображении на карте. Это улучшит у игрока связь между предметом и его описанием. Мы рисуем предмет на экране в соответствующем слоте инвентаря. Если слот пуст, то будет отображен только номер слота, в нашем случае, это одна из букв, от 'a' до 'z'. Давайте пройдемся по каждой из функций, которые мы здесь называли.

character.bi

'Возвращает «истина», если в слоте инвентаря есть предмет.
Function character.HasInvItem(idx As Integer) As Integer
    'Проверим на правильность индекса.
    If idx >= Lbound(_cinfo.cinv) And idx <= Ubound(_cinfo.cinv) Then
        'Проверим идентификатор класса предмета.
        If _cinfo.cinv(idx).classid = clNone Then
            Return FALSE
        Else
            Return TRUE
        Endif
    Else
        Return FALSE
    End If

End Function


Здесь необходимо небольшое пояснение. Мы проверяем, равен ли идентификатор класса предмета clNone и если это верно, то возвращаем «ложь», иначе «истина».

inv.bi

'Возвращает «истина», если предмет опознан.
Function IsEval(inv As invtype) As Integer
    Dim As Integer ret

    'Если нечего помечать, как неопознанное.
    If inv.classid = clNone Then
        ret = TRUE
    Else
        'Проверим предметы различного типа.
        Select Case inv.classid 'Золото ненужно распознавать.
            Case clGold
                ret = TRUE
            Case clSupplies  'Вернуть значение поля eval.
                ret = inv.supply.eval
        End Select
    Endif

    Return ret
End Function


Функуия IsEval просто возвращает значение флага eval предмета из инвентаря. Так как сейчас у нас есть только предметы класса Supply, то мы проверяем только их. Позже мы будем расширять данную функцию.

character.bi

'Получить предмет из слота инвентаря.
Sub character.GetInventoryItem(idx As Integer, inv As invtype)

    'Очистим переменную для предмета.
    ClearInv inv
    'Проверим правильность индекса.
    If idx >= Lbound(_cinfo.cinv) And idx <= Ubound(_cinfo.cinv) Then
        inv = _cinfo.cinv(idx)
    End If
End Sub


Мы уже видели это ранее. Мы проверяем индекс, чтобы убедится что он лежит в нужном диапазоне, и возвращаем предмет, находящийся в слоте по этому индексу. Мы не проверяем — свободный слот или нет, чтобы использовать данную функцию как для получения значений из пустых слотов инвентаря (как мы делали это в предыдущей главе), так и содержащих предметы, как мы делаем здесь.

inv.bi

'Возвращает описание предмета, находящегося по координатам x,y.
Function GetInvItemDesc(inv As invtype) As String
    Dim As String ret = "None"

    'Если идентификатор класса предмета clNone, то ничего не делаем.
    If inv.classid <> clNone Then
        'Возбмем описание для золота.
        If inv.classid = clGold Then
            ret = inv.desc
        Endif
    Endif
    'Описание для «ады».
    If inv.classid = clSupplies Then
        'Если не опознано, то вернуть основное описание.
        If inv.supply.eval = FALSE Then
            ret = inv.desc
        Else
            'Вернем секретное описание.
            ret = inv.supply.sdesc
        Endif
    Endif

    Return ret
End Function


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

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

dod.bas

'Команда распознания предмета.
Function ProcessEval() As Integer
    Dim As String res, mask, desc
    Dim As Integer i, iret, iitem, evaldr, pint, rollp, rolle, ret = FALSE
    Dim As invtype inv
    Dim As tWidgets.btnID btn
    Dim As tWidgets.tInputbox ib

    'Убедимся, что у нас есть что распознавать.
    For i = pchar.LowInv To pchar.HighInv
        iitem = pchar.HasInvItem(i)
        If iitem = TRUE Then
            'получим предмет инвентаря.
            pchar.GetInventoryItem i, inv
            'распознан ли он.
            iret = IsEval(inv)
            'необходимо распознать.
            If iret = FALSE Then
                'Построим маску слотов.
                mask &= Chr(i)
            End If
        Endif
    Next
    If Len(mask) = 0 Then
        ShowMsg "Evaluate", "Nothing to evaluate.", tWidgets.MsgBoxType.gmbOK
    Else
        'Написовать строку ввода на экране.
        ib.Title = "Evaluate"
        ib.Prompt = "Select item(s) to evaluate (" & mask & ")"
        ib.Row = 39
        ib.EditMask = mask
        ib.MaxLen = Len(mask)
        ib.InputLen = Len(mask)
        btn = ib.Inputbox(res)
        'Распознаем каждый жлемент в списке.
        If (btn <> tWidgets.btnID.gbnCancel) And (Len(res) > 0) Then
            'Список на распознание.
            For i = 1 To Len(res)о
                iitem = Asc(res, i) 'Получим индекс слота в инвентаре персонажа.
                'Получим предмет из инвентаря
                pchar.GetInventoryItem iitem, inv
                'Получим сложность распознавания.
                evalDR = GetEvalDR(inv)
                'Используя интеллект получим возможность распознавания.
                pint = pchar.CurrInt + pchar.BonInt
                'Случайное число для сложности распознавания.
                rolle = RandomRange(0, evalDR)
                'Случайное сичло для возможности распознавания игроком.
                rollp = RandomRange(0, pint)
                'Получим описание предмета.
                desc = GetInvItemDesc(inv)
                'Если случайное число у игрока > числу необходимому для распознания - распознаем
                If rollp > rolle Then
                    desc &= " was succesfully evaluated."
                    ShowMsg "Evaluate", desc, tWidgets.MsgBoxType.gmbOK
                    SetInvEval inv, TRUE 'Set eval to true.
                    pchar.AddInvItem iitem, inv 'Put item back into inv.
                    ret = TRUE 'Flag caller that screen has changed.
                Else
                    desc &= " was not evaluated."
                    ShowMsg "Evaluate", desc, tWidgets.MsgBoxType.gmbOK
                Endif
            Next
        Endif
    Endif

    Return ret
End Function


Во первых, вы заметите некоторые определения из пространства имен tWidgets. Они содержатся в файле tWidgets.bi. Twidgets содержит в себе набор виджетов, которые реализуют GUI (Графические Интерфейс Пользователя) в текстовом видео режиме. Я не буду обсуждать здесь виджеты, но они полностью описаны в разделе приложений.

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

Как только у нас есть список предметов, игрок выбирает предмет, который хочет распознать. Процесс распознавания выглядит следующим образом:
Получить evalDR (рейтинг сложности) предмета для распознания: evalDR = GetEvalDR (INV).
Получить значение интеллекта персонажа вместе с бонусом: pint = pchar.CurrInt + pchar.BonInt
Получить случайное число от 0 до evalDR: rolle = RandomRange(0, evalDR)
Получить случайное число от 0 до полученного интеллекта персонажа с бонусом: rollp = RandomRange(0, pint)
Если число, выпавшее персонажу от его интеллекта больше числа, выпавшего от рейтинга сложности, распознавание прошло успешно. If rollp > rolle
Если распознавание успешно, то выставим у предмета поле eval в состояние «истина» и информируем об этом игрока. SetInvEval inv, TRUE
Если распознавание прошло не удачно, то сообщаем об этом игроку.

Мы получаем Рейтинг сложности используя функцию GetEvalDR

inv.bi

'Возвразает рейтинг сложности распознания предмета.
Function GetEvalDR(inv As invtype) As Integer
    Dim As Integer ret

    'Если нечего распознавать, то вернем 0.
    If inv.classid = clNone Then
        ret = 0
    Else
        'Выберем тип предмета.
        Select Case inv.classid
            Case clGold 'Золото не нужно распознавать .
                ret = 0
            Case clSupplies
                ret = inv.supply.evaldr 'Вернем значение evaldr для clSupplies.
        End Select
    Endif

    Return ret
End Function


Мы просто возвращаем значение поля evaldr если есть предмет, требующий распознавания, иначе возвращаем 0. После того, как предмет распознан, нам нужно изменить у него значение поля evaldr, для чего мы используем процедуру SetInvEval.

inv.bi

'Устанавливает значение eval, для предмета inv.
Sub SetInvEval(inv As invtype, state As Integer)

    'проверим, есть ли предмет
    If inv.classid <> clNone Then
        'Выберем предмет.
        Select Case inv.classid
            Case clSupplies
                inv.supply.eval = state 'Установим значение eval.
        End Select
    Endif
End Sub


Здесь мы просто устанавливаем значение поля eval в то, которое передали в процедуру. Это дает нам возможность установить значение в «не распознано», если мы захотим реализовать эту функциональность. Мы могли бы это сделать, как результат заклинания монстра или воздействия чего либо еще. Мы пока еще не решили, нужно ли нам это, но лучше оставить свободу выбора, если позже данный функционал нам понадобиться.

Функция распознавания возвращает «истина» в ManageInventory, для того, чтобы мы могли перерисовать экран для отображения обновленного состояния предметов. Это даст игроку все время видеть в инвентаре текущее состояние всех предметов.

dod.bas

'Управление инвентарем персонажа.
Sub ManageInventory()
    Dim As String kch, ich
    Dim As Integer ret

    DrawInventoryScreen
    Do
        kch = Inkey
        kch = Ucase(kch)
        'Проверим, была ли нажата клавиша.
        If kch <> "" Then
            'Команда распознавания предметов.
            If kch = "E" Then
                ret = ProcessEval()
                'Экран изменен.
                If ret = TRUE Then
                    DrawInventoryScreen
                Endif
            Endif
        Endif
        Sleep 1
    Loop Until kch = key_esc
    ClearKeys
End Sub


Есть и другие команды для инвентаря, которые мы пока не реализовали, так как у нас пока ограниченный набор предметов. Например команда «Взять в руки/Одеть» относится только к оружию, броне, ожерельям и кольцам. Команда «Прочитать» действительна только для свитков и книг с заклинаниями, которых у нас также нет на данный момент. Тем не менее, команды «Выбросить», «Съесть/Выпить» и «Осмотреть» мы можем реализовать уже с теми предметами, которые у нас есть на данный момент. Этим мы и займемся в следующей главе.

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

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