From 8d1708358f6dd60f87259f99d91d8e9876232c06 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Tue, 23 Nov 2021 16:31:27 +0000 Subject: [PATCH] Extract spell list and book into separate files Moves the spell list/book UI from `control.cpp` code into separate files. --- CMakeLists.txt | 3 + Source/control.cpp | 674 +--------------------------------- Source/control.h | 9 - Source/controls/plrctrls.cpp | 1 + Source/diablo.cpp | 3 +- Source/panels/spell_book.cpp | 212 +++++++++++ Source/panels/spell_book.hpp | 12 + Source/panels/spell_icons.cpp | 152 ++++++++ Source/panels/spell_icons.hpp | 37 ++ Source/panels/spell_list.cpp | 372 +++++++++++++++++++ Source/panels/spell_list.hpp | 26 ++ Source/spell_book.cpp | 212 +++++++++++ Source/spell_icons.cpp | 152 ++++++++ Source/spell_list.cpp | 372 +++++++++++++++++++ 14 files changed, 1561 insertions(+), 676 deletions(-) create mode 100644 Source/panels/spell_book.cpp create mode 100644 Source/panels/spell_book.hpp create mode 100644 Source/panels/spell_icons.cpp create mode 100644 Source/panels/spell_icons.hpp create mode 100644 Source/panels/spell_list.cpp create mode 100644 Source/panels/spell_list.hpp create mode 100644 Source/spell_book.cpp create mode 100644 Source/spell_icons.cpp create mode 100644 Source/spell_list.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7aae23548..8879cd3ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -506,6 +506,9 @@ set(libdevilutionx_SRCS Source/DiabloUI/title.cpp Source/panels/charpanel.cpp Source/panels/mainpanel.cpp + Source/panels/spell_book.cpp + Source/panels/spell_icons.cpp + Source/panels/spell_list.cpp Source/dvlnet/abstract_net.cpp Source/dvlnet/base.cpp Source/dvlnet/cdwrap.cpp diff --git a/Source/control.cpp b/Source/control.cpp index 35503e922..a16037486 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -29,8 +29,12 @@ #include "lighting.h" #include "minitext.h" #include "missiles.h" +#include "options.h" #include "panels/charpanel.hpp" #include "panels/mainpanel.hpp" +#include "panels/spell_book.hpp" +#include "panels/spell_icons.hpp" +#include "panels/spell_list.hpp" #include "qol/xpbar.h" #include "stores.h" #include "towners.h" @@ -39,7 +43,6 @@ #include "utils/sdl_geometry.h" #include "utils/stdcompat/optional.hpp" #include "utils/utf8.hpp" -#include "options.h" #ifdef _DEBUG #include "debug.h" @@ -124,10 +127,6 @@ std::optional pDurIcons; std::optional multiButtons; std::optional pPanelButtons; std::optional pGBoxBuff; -std::optional pSBkBtnCel; -std::optional pSBkIconCels; -std::optional pSpellBkCel; -std::optional pSpellCels; bool PanelButtons[8]; int PanelButtonIndex; @@ -139,68 +138,6 @@ bool TalkButtonsDown[3]; int sgbPlrTalkTbl; bool WhisperList[MAX_PLRS]; char panelstr[4][64]; -uint8_t SplTransTbl[256]; - -constexpr int SpellBookDescriptionWidth = 250; -constexpr int SpellBookDescriptionHeight = 43; -constexpr int SpellBookDescriptionPaddingLeft = 2; -constexpr int SpellBookDescriptionPaddingRight = 2; - -/** Maps from spell_id to spelicon.cel frame number. */ -char SpellITbl[] = { - 27, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 28, - 13, - 12, - 18, - 16, - 14, - 18, - 19, - 11, - 20, - 15, - 21, - 23, - 24, - 25, - 22, - 26, - 29, - 37, - 38, - 39, - 42, - 41, - 40, - 10, - 36, - 30, - 51, - 51, - 50, - 46, - 47, - 43, - 45, - 48, - 49, - 44, - 35, - 35, - 35, - 35, - 35, -}; enum panel_button_id { PanelButtonCharinfo, @@ -227,20 +164,6 @@ const char *const PanBtnStr[8] = { "" // Player attack }; -/** Maps from spellbook page number and position to spell_id. */ -spell_id SpellPages[6][7] = { - { SPL_NULL, SPL_FIREBOLT, SPL_CBOLT, SPL_HBOLT, SPL_HEAL, SPL_HEALOTHER, SPL_FLAME }, - { SPL_RESURRECT, SPL_FIREWALL, SPL_TELEKINESIS, SPL_LIGHTNING, SPL_TOWN, SPL_FLASH, SPL_STONE }, - { SPL_RNDTELEPORT, SPL_MANASHIELD, SPL_ELEMENT, SPL_FIREBALL, SPL_WAVE, SPL_CHAIN, SPL_GUARDIAN }, - { SPL_NOVA, SPL_GOLEM, SPL_TELEPORT, SPL_APOCA, SPL_BONESPIRIT, SPL_FLARE, SPL_ETHEREALIZE }, - { SPL_LIGHTWALL, SPL_IMMOLAT, SPL_WARP, SPL_REFLECT, SPL_BERSERK, SPL_FIRERING, SPL_SEARCH }, - { SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID } -}; - -#define SPLICONLENGTH 56 -#define SPLROWICONLS 10 -#define SPLICONLAST (gbIsHellfire ? 52 : 43) - void CalculatePanelAreas() { MainPanel = { @@ -273,118 +196,6 @@ void CalculatePanelAreas() RightPanel.position.y = LeftPanel.position.y; } -/** - * Draw spell cell onto the given buffer. - * @param out Output buffer - * @param position Buffer coordinates - * @param cel The CEL sprite - * @param nCel Index of the cel frame to draw. 0 based. - */ -void DrawSpellCel(const Surface &out, Point position, const CelSprite &cel, int nCel) -{ - CelDrawLightTo(out, position, cel, nCel, SplTransTbl); -} - -void SetSpellTrans(spell_type t) -{ - if (t == RSPLTYPE_SKILL) { - for (int i = 0; i < 128; i++) - SplTransTbl[i] = i; - } - for (int i = 128; i < 256; i++) - SplTransTbl[i] = i; - SplTransTbl[255] = 0; - - switch (t) { - case RSPLTYPE_SPELL: - SplTransTbl[PAL8_YELLOW] = PAL16_BLUE + 1; - SplTransTbl[PAL8_YELLOW + 1] = PAL16_BLUE + 3; - SplTransTbl[PAL8_YELLOW + 2] = PAL16_BLUE + 5; - for (int i = PAL16_BLUE; i < PAL16_BLUE + 16; i++) { - SplTransTbl[PAL16_BEIGE - PAL16_BLUE + i] = i; - SplTransTbl[PAL16_YELLOW - PAL16_BLUE + i] = i; - SplTransTbl[PAL16_ORANGE - PAL16_BLUE + i] = i; - } - break; - case RSPLTYPE_SCROLL: - SplTransTbl[PAL8_YELLOW] = PAL16_BEIGE + 1; - SplTransTbl[PAL8_YELLOW + 1] = PAL16_BEIGE + 3; - SplTransTbl[PAL8_YELLOW + 2] = PAL16_BEIGE + 5; - for (int i = PAL16_BEIGE; i < PAL16_BEIGE + 16; i++) { - SplTransTbl[PAL16_YELLOW - PAL16_BEIGE + i] = i; - SplTransTbl[PAL16_ORANGE - PAL16_BEIGE + i] = i; - } - break; - case RSPLTYPE_CHARGES: - SplTransTbl[PAL8_YELLOW] = PAL16_ORANGE + 1; - SplTransTbl[PAL8_YELLOW + 1] = PAL16_ORANGE + 3; - SplTransTbl[PAL8_YELLOW + 2] = PAL16_ORANGE + 5; - for (int i = PAL16_ORANGE; i < PAL16_ORANGE + 16; i++) { - SplTransTbl[PAL16_BEIGE - PAL16_ORANGE + i] = i; - SplTransTbl[PAL16_YELLOW - PAL16_ORANGE + i] = i; - } - break; - case RSPLTYPE_INVALID: - SplTransTbl[PAL8_YELLOW] = PAL16_GRAY + 1; - SplTransTbl[PAL8_YELLOW + 1] = PAL16_GRAY + 3; - SplTransTbl[PAL8_YELLOW + 2] = PAL16_GRAY + 5; - for (int i = PAL16_GRAY; i < PAL16_GRAY + 15; i++) { - SplTransTbl[PAL16_BEIGE - PAL16_GRAY + i] = i; - SplTransTbl[PAL16_YELLOW - PAL16_GRAY + i] = i; - SplTransTbl[PAL16_ORANGE - PAL16_GRAY + i] = i; - } - SplTransTbl[PAL16_BEIGE + 15] = 0; - SplTransTbl[PAL16_YELLOW + 15] = 0; - SplTransTbl[PAL16_ORANGE + 15] = 0; - break; - case RSPLTYPE_SKILL: - break; - } -} - -void PrintSBookSpellType(const Surface &out, Point position, const std::string &text, uint8_t rectColorIndex) -{ - Point rect { position }; - rect += Displacement { 0, -SPLICONLENGTH + 1 }; - - // Top - DrawHorizontalLine(out, rect, SPLICONLENGTH, rectColorIndex); - DrawHorizontalLine(out, rect + Displacement { 0, 1 }, SPLICONLENGTH, rectColorIndex); - - // Bottom - DrawHorizontalLine(out, rect + Displacement { 0, SPLICONLENGTH - 2 }, SPLICONLENGTH, rectColorIndex); - DrawHorizontalLine(out, rect + Displacement { 0, SPLICONLENGTH - 1 }, SPLICONLENGTH, rectColorIndex); - - // Left Side - DrawVerticalLine(out, rect, SPLICONLENGTH, rectColorIndex); - DrawVerticalLine(out, rect + Displacement { 1, 0 }, SPLICONLENGTH, rectColorIndex); - - // Right Side - DrawVerticalLine(out, rect + Displacement { SPLICONLENGTH - 2, 0 }, SPLICONLENGTH, rectColorIndex); - DrawVerticalLine(out, rect + Displacement { SPLICONLENGTH - 1, 0 }, SPLICONLENGTH, rectColorIndex); - - // Align the spell type text with bottom of spell icon - position += Displacement { SPLICONLENGTH / 2 - GetLineWidth(text.c_str()) / 2, (IsSmallFontTall() ? -19 : -15) }; - - // Draw a drop shadow below and to the left of the text - DrawString(out, text, position + Displacement { -1, 1 }, UiFlags::ColorBlack); - DrawString(out, text, position + Displacement { -1, -1 }, UiFlags::ColorBlack); - DrawString(out, text, position + Displacement { 1, -1 }, UiFlags::ColorBlack); - // Then draw the text over the top - DrawString(out, text, position, UiFlags::ColorWhite); -} - -void PrintSBookHotkey(const Surface &out, Point position, const std::string &text) -{ - // Align the hot key text with the top-right corner of the spell icon - position += Displacement { SPLICONLENGTH - (GetLineWidth(text.c_str()) + 5), 5 - SPLICONLENGTH }; - - // Draw a drop shadow below and to the left of the text - DrawString(out, text, position + Displacement { -1, 1 }, UiFlags::ColorBlack); - // Then draw the text over the top - DrawString(out, text, position, UiFlags::ColorWhite); -} - /** * Draws a section of the empty flask cel on top of the panel to create the illusion * of the flask getting empty. This function takes a cel and draws a @@ -526,41 +337,6 @@ int DrawDurIcon4Item(const Surface &out, Item &pItem, int x, int c) return x - 32 - 8; } -void PrintSBookStr(const Surface &out, Point position, string_view text, UiFlags flags = UiFlags::None) -{ - DrawString(out, text, - { GetPanelPosition(UiPanels::Spell, { SPLICONLENGTH + SpellBookDescriptionPaddingLeft + position.x, position.y }), - { SpellBookDescriptionWidth - SpellBookDescriptionPaddingLeft - SpellBookDescriptionPaddingRight, 0 } }, - UiFlags::ColorWhite | flags); -} - -spell_type GetSBookTrans(spell_id ii, bool townok) -{ - auto &myPlayer = Players[MyPlayerId]; - if ((myPlayer._pClass == HeroClass::Monk) && (ii == SPL_SEARCH)) - return RSPLTYPE_SKILL; - spell_type st = RSPLTYPE_SPELL; - if ((myPlayer._pISpells & GetSpellBitmask(ii)) != 0) { - st = RSPLTYPE_CHARGES; - } - if ((myPlayer._pAblSpells & GetSpellBitmask(ii)) != 0) { - st = RSPLTYPE_SKILL; - } - if (st == RSPLTYPE_SPELL) { - if (CheckSpell(MyPlayerId, ii, st, true) != SpellCheckResult::Success) { - st = RSPLTYPE_INVALID; - } - if ((char)(myPlayer._pSplLvl[ii] + myPlayer._pISplLvlAdd) <= 0) { - st = RSPLTYPE_INVALID; - } - } - if (townok && currlevel == 0 && st != RSPLTYPE_INVALID && !spelldata[ii].sTownSpell) { - st = RSPLTYPE_INVALID; - } - - return st; -} - void ControlSetGoldCurs(Player &player) { SetPlrHandGoldCurs(player.HoldItem); @@ -639,25 +415,6 @@ void RemoveGold(Player &player, int goldIndex) dropGoldValue = 0; } -bool GetSpellListSelection(spell_id &pSpell, spell_type &pSplType) -{ - pSpell = spell_id::SPL_INVALID; - pSplType = spell_type::RSPLTYPE_INVALID; - auto &myPlayer = Players[MyPlayerId]; - - for (auto &spellListItem : GetSpellListItems()) { - if (spellListItem.isSelected) { - pSpell = spellListItem.id; - pSplType = spellListItem.type; - if (myPlayer._pClass == HeroClass::Monk && spellListItem.id == SPL_SEARCH) - pSplType = RSPLTYPE_SKILL; - return true; - } - } - - return false; -} - } // namespace bool IsChatAvailable() @@ -669,238 +426,6 @@ bool IsChatAvailable() #endif } -void DrawSpell(const Surface &out) -{ - auto &myPlayer = Players[MyPlayerId]; - spell_id spl = myPlayer._pRSpell; - spell_type st = myPlayer._pRSplType; - - // BUGFIX: Move the next line into the if statement to avoid OOB (SPL_INVALID is -1) (fixed) - if (st == RSPLTYPE_SPELL && spl != SPL_INVALID) { - int tlvl = myPlayer._pISplLvlAdd + myPlayer._pSplLvl[spl]; - if (CheckSpell(MyPlayerId, spl, st, true) != SpellCheckResult::Success) - st = RSPLTYPE_INVALID; - if (tlvl <= 0) - st = RSPLTYPE_INVALID; - } - if (currlevel == 0 && st != RSPLTYPE_INVALID && !spelldata[spl].sTownSpell) - st = RSPLTYPE_INVALID; - SetSpellTrans(st); - const int nCel = (spl != SPL_INVALID) ? SpellITbl[spl] : 27; - const Point position { PANEL_X + 565, PANEL_Y + 119 }; - DrawSpellCel(out, position, *pSpellCels, nCel); -} - -void DrawSpellList(const Surface &out) -{ - infostr[0] = '\0'; - ClearPanel(); - - auto &myPlayer = Players[MyPlayerId]; - - for (auto &spellListItem : GetSpellListItems()) { - const spell_id spellId = spellListItem.id; - spell_type transType = spellListItem.type; - int spellLevel = 0; - const SpellData &spellDataItem = spelldata[static_cast(spellListItem.id)]; - if (currlevel == 0 && !spellDataItem.sTownSpell) { - transType = RSPLTYPE_INVALID; - } - if (spellListItem.type == RSPLTYPE_SPELL) { - spellLevel = std::max(myPlayer._pISplLvlAdd + myPlayer._pSplLvl[spellListItem.id], 0); - if (spellLevel == 0) - transType = RSPLTYPE_INVALID; - } - - SetSpellTrans(transType); - DrawSpellCel(out, spellListItem.location, *pSpellCels, SpellITbl[static_cast(spellId)]); - - if (!spellListItem.isSelected) - continue; - - uint8_t spellColor = PAL16_GRAY + 5; - - switch (spellListItem.type) { - case RSPLTYPE_SKILL: - spellColor = PAL16_YELLOW - 46; - PrintSBookSpellType(out, spellListItem.location, _("Skill"), spellColor); - strcpy(infostr, fmt::format(_("{:s} Skill"), pgettext("spell", spellDataItem.sSkillText)).c_str()); - break; - case RSPLTYPE_SPELL: - if (myPlayer.plrlevel != 0) { - spellColor = PAL16_BLUE + 5; - } - PrintSBookSpellType(out, spellListItem.location, _("Spell"), spellColor); - strcpy(infostr, fmt::format(_("{:s} Spell"), pgettext("spell", spellDataItem.sNameText)).c_str()); - if (spellId == SPL_HBOLT) { - strcpy(tempstr, _("Damages undead only")); - AddPanelString(tempstr); - } - if (spellLevel == 0) - strcpy(tempstr, _("Spell Level 0 - Unusable")); - else - strcpy(tempstr, fmt::format(_("Spell Level {:d}"), spellLevel).c_str()); - AddPanelString(tempstr); - break; - case RSPLTYPE_SCROLL: { - if (myPlayer.plrlevel != 0) { - spellColor = PAL16_RED - 59; - } - PrintSBookSpellType(out, spellListItem.location, _("Scroll"), spellColor); - strcpy(infostr, fmt::format(_("Scroll of {:s}"), pgettext("spell", spellDataItem.sNameText)).c_str()); - const InventoryAndBeltPlayerItemsRange items { myPlayer }; - const int scrollCount = std::count_if(items.begin(), items.end(), [spellId](const Item &item) { - return item.IsScrollOf(spellId); - }); - strcpy(tempstr, fmt::format(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount), scrollCount).c_str()); - AddPanelString(tempstr); - } break; - case RSPLTYPE_CHARGES: { - if (myPlayer.plrlevel != 0) { - spellColor = PAL16_ORANGE + 5; - } - PrintSBookSpellType(out, spellListItem.location, _("Staff"), spellColor); - strcpy(infostr, fmt::format(_("Staff of {:s}"), pgettext("spell", spellDataItem.sNameText)).c_str()); - int charges = myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges; - strcpy(tempstr, fmt::format(ngettext("{:d} Charge", "{:d} Charges", charges), charges).c_str()); - AddPanelString(tempstr); - } break; - case RSPLTYPE_INVALID: - break; - } - for (int t = 0; t < 4; t++) { - if (myPlayer._pSplHotKey[t] == spellId && myPlayer._pSplTHotKey[t] == spellListItem.type) { - auto hotkeyName = keymapper.KeyNameForAction(quickSpellActionIndexes[t]); - PrintSBookHotkey(out, spellListItem.location, hotkeyName); - strcpy(tempstr, fmt::format(_("Spell Hotkey {:s}"), hotkeyName.c_str()).c_str()); - AddPanelString(tempstr); - } - } - } -} - -std::vector GetSpellListItems() -{ - std::vector spellListItems; - - uint64_t mask; - - int x = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; - int y = PANEL_Y - 17; - - for (int i = RSPLTYPE_SKILL; i < RSPLTYPE_INVALID; i++) { - auto &myPlayer = Players[MyPlayerId]; - switch ((spell_type)i) { - case RSPLTYPE_SKILL: - mask = myPlayer._pAblSpells; - break; - case RSPLTYPE_SPELL: - mask = myPlayer._pMemSpells; - break; - case RSPLTYPE_SCROLL: - mask = myPlayer._pScrlSpells; - break; - case RSPLTYPE_CHARGES: - mask = myPlayer._pISpells; - break; - case RSPLTYPE_INVALID: - break; - } - int8_t j = SPL_FIREBOLT; - for (uint64_t spl = 1; j < MAX_SPELLS; spl <<= 1, j++) { - if ((mask & spl) == 0) - continue; - int lx = x; - int ly = y - SPLICONLENGTH; - bool isSelected = (MousePosition.x >= lx && MousePosition.x < lx + SPLICONLENGTH && MousePosition.y >= ly && MousePosition.y < ly + SPLICONLENGTH); - spellListItems.emplace_back(SpellListItem { { x, y }, (spell_type)i, (spell_id)j, isSelected }); - x -= SPLICONLENGTH; - if (x == PANEL_X + 12 - SPLICONLENGTH) { - x = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; - y -= SPLICONLENGTH; - } - } - if (mask != 0 && x != PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS) - x -= SPLICONLENGTH; - if (x == PANEL_X + 12 - SPLICONLENGTH) { - x = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; - y -= SPLICONLENGTH; - } - } - - return spellListItems; -} - -void SetSpell() -{ - spell_id pSpell; - spell_type pSplType; - - spselflag = false; - if (!GetSpellListSelection(pSpell, pSplType)) { - return; - } - - ClearPanel(); - - auto &myPlayer = Players[MyPlayerId]; - myPlayer._pRSpell = pSpell; - myPlayer._pRSplType = pSplType; - - force_redraw = 255; -} - -void SetSpeedSpell(int slot) -{ - spell_id pSpell; - spell_type pSplType; - - if (!GetSpellListSelection(pSpell, pSplType)) { - return; - } - auto &myPlayer = Players[MyPlayerId]; - for (int i = 0; i < 4; ++i) { - if (myPlayer._pSplHotKey[i] == pSpell && myPlayer._pSplTHotKey[i] == pSplType) - myPlayer._pSplHotKey[i] = SPL_INVALID; - } - myPlayer._pSplHotKey[slot] = pSpell; - myPlayer._pSplTHotKey[slot] = pSplType; -} - -void ToggleSpell(int slot) -{ - uint64_t spells; - - auto &myPlayer = Players[MyPlayerId]; - - if (myPlayer._pSplHotKey[slot] == SPL_INVALID) { - return; - } - - switch (myPlayer._pSplTHotKey[slot]) { - case RSPLTYPE_SKILL: - spells = myPlayer._pAblSpells; - break; - case RSPLTYPE_SPELL: - spells = myPlayer._pMemSpells; - break; - case RSPLTYPE_SCROLL: - spells = myPlayer._pScrlSpells; - break; - case RSPLTYPE_CHARGES: - spells = myPlayer._pISpells; - break; - case RSPLTYPE_INVALID: - return; - } - - if ((spells & GetSpellBitmask(myPlayer._pSplHotKey[slot])) != 0) { - myPlayer._pRSpell = myPlayer._pSplHotKey[slot]; - myPlayer._pRSplType = myPlayer._pSplTHotKey[slot]; - force_redraw = 255; - } -} - void AddPanelString(string_view str) { CopyUtf8(panelstr[pnumlines], str, sizeof(*panelstr)); @@ -974,12 +499,7 @@ void InitControlPan() pLifeBuff.emplace(88, 88); LoadCharPanel(); - - if (!gbIsHellfire) - pSpellCels = LoadCel("CtrlPan\\SpelIcon.CEL", SPLICONLENGTH); - else - pSpellCels = LoadCel("Data\\SpelIcon.CEL", SPLICONLENGTH); - SetSpellTrans(RSPLTYPE_SKILL); + LoadSpellIcons(); CelDrawUnsafeTo(*pBtmBuff, { 0, (PANEL_HEIGHT + 16) - 1 }, LoadCel("CtrlPan\\Panel8.CEL", PANEL_WIDTH), 1); { const Point bulbsPosition { 0, 87 }; @@ -1019,33 +539,10 @@ void InitControlPan() drawmanaflag = true; chrflag = false; spselflag = false; - pSpellBkCel = LoadCel("Data\\SpellBk.CEL", SPANEL_WIDTH); - - if (gbIsHellfire) { - static const int SBkBtnHellfireWidths[] = { 0, 61, 61, 61, 61, 61, 76 }; - pSBkBtnCel = LoadCel("Data\\SpellBkB.CEL", SBkBtnHellfireWidths); - } else { - pSBkBtnCel = LoadCel("Data\\SpellBkB.CEL", 76); - } - pSBkIconCels = LoadCel("Data\\SpellI2.CEL", 37); sbooktab = 0; sbookflag = false; - auto &myPlayer = Players[MyPlayerId]; - - if (myPlayer._pClass == HeroClass::Warrior) { - SpellPages[0][0] = SPL_REPAIR; - } else if (myPlayer._pClass == HeroClass::Rogue) { - SpellPages[0][0] = SPL_DISARM; - } else if (myPlayer._pClass == HeroClass::Sorcerer) { - SpellPages[0][0] = SPL_RECHARGE; - } else if (myPlayer._pClass == HeroClass::Monk) { - SpellPages[0][0] = SPL_SEARCH; - } else if (myPlayer._pClass == HeroClass::Bard) { - SpellPages[0][0] = SPL_IDENTIFY; - } else if (myPlayer._pClass == HeroClass::Barbarian) { - SpellPages[0][0] = SPL_BLODBOIL; - } + InitSpellBook(); pQLogCel = LoadCel("Data\\Quest.CEL", SPANEL_WIDTH); pGBoxBuff = LoadCel("CtrlPan\\Golddrop.cel", 261); CloseGoldDrop(); @@ -1082,60 +579,6 @@ void DrawCtrlBtns(const Surface &out) } } -void DoSpeedBook() -{ - spselflag = true; - int xo = PANEL_X + 12 + SPLICONLENGTH * 10; - int yo = PANEL_Y - 17; - int x = xo + SPLICONLENGTH / 2; - int y = yo - SPLICONLENGTH / 2; - - auto &myPlayer = Players[MyPlayerId]; - - if (myPlayer._pRSpell != SPL_INVALID) { - for (int i = RSPLTYPE_SKILL; i <= RSPLTYPE_CHARGES; i++) { - uint64_t spells; - switch (i) { - case RSPLTYPE_SKILL: - spells = myPlayer._pAblSpells; - break; - case RSPLTYPE_SPELL: - spells = myPlayer._pMemSpells; - break; - case RSPLTYPE_SCROLL: - spells = myPlayer._pScrlSpells; - break; - case RSPLTYPE_CHARGES: - spells = myPlayer._pISpells; - break; - } - uint64_t spell = 1; - for (int j = 1; j < MAX_SPELLS; j++) { - if ((spell & spells) != 0) { - if (j == myPlayer._pRSpell && i == myPlayer._pRSplType) { - x = xo + SPLICONLENGTH / 2; - y = yo - SPLICONLENGTH / 2; - } - xo -= SPLICONLENGTH; - if (xo == PANEL_X + 12 - SPLICONLENGTH) { - xo = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; - yo -= SPLICONLENGTH; - } - } - spell <<= 1ULL; - } - if (spells != 0 && xo != PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS) - xo -= SPLICONLENGTH; - if (xo == PANEL_X + 12 - SPLICONLENGTH) { - xo = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; - yo -= SPLICONLENGTH; - } - } - } - - SetCursorPos({ x, y }); -} - void ClearPanBtn() { for (bool &panelButton : PanelButtons) @@ -1362,16 +805,14 @@ void FreeControlPan() pBtmBuff = std::nullopt; pManaBuff = std::nullopt; pLifeBuff = std::nullopt; - pSpellCels = std::nullopt; + FreeSpellIcons(); + FreeSpellBook(); pPanelButtons = std::nullopt; multiButtons = std::nullopt; talkButtons = std::nullopt; pChrButtons = std::nullopt; pDurIcons = std::nullopt; pQLogCel = std::nullopt; - pSpellBkCel = std::nullopt; - pSBkBtnCel = std::nullopt; - pSBkIconCels = std::nullopt; pGBoxBuff = std::nullopt; FreeMainPanel(); FreeCharPanel(); @@ -1558,105 +999,6 @@ void RedBack(const Surface &out) } } -void DrawSpellBook(const Surface &out) -{ - CelDrawTo(out, GetPanelPosition(UiPanels::Spell, { 0, 351 }), *pSpellBkCel, 1); - if (gbIsHellfire && sbooktab < 5) { - CelDrawTo(out, GetPanelPosition(UiPanels::Spell, { 61 * sbooktab + 7, 348 }), *pSBkBtnCel, sbooktab + 1); - } else { - // BUGFIX: rendering of page 3 and page 4 buttons are both off-by-one pixel (fixed). - int sx = 76 * sbooktab + 7; - if (sbooktab == 2 || sbooktab == 3) { - sx++; - } - CelDrawTo(out, GetPanelPosition(UiPanels::Spell, { sx, 348 }), *pSBkBtnCel, sbooktab + 1); - } - auto &myPlayer = Players[MyPlayerId]; - uint64_t spl = myPlayer._pMemSpells | myPlayer._pISpells | myPlayer._pAblSpells; - - const int lineHeight = 18; - - int yp = 12; - const int textPaddingTop = 7; - for (int i = 1; i < 8; i++) { - spell_id sn = SpellPages[sbooktab][i - 1]; - if (sn != SPL_INVALID && (spl & GetSpellBitmask(sn)) != 0) { - spell_type st = GetSBookTrans(sn, true); - SetSpellTrans(st); - const Point spellCellPosition = GetPanelPosition(UiPanels::Spell, { 11, yp + SpellBookDescriptionHeight }); - DrawSpellCel(out, spellCellPosition, *pSBkIconCels, SpellITbl[sn]); - if (sn == myPlayer._pRSpell && st == myPlayer._pRSplType) { - SetSpellTrans(RSPLTYPE_SKILL); - DrawSpellCel(out, spellCellPosition, *pSBkIconCels, SPLICONLAST); - } - - const Point line0 { 0, yp + textPaddingTop }; - const Point line1 { 0, yp + textPaddingTop + lineHeight }; - PrintSBookStr(out, line0, pgettext("spell", spelldata[sn].sNameText)); - switch (GetSBookTrans(sn, false)) { - case RSPLTYPE_SKILL: - PrintSBookStr(out, line1, _("Skill")); - break; - case RSPLTYPE_CHARGES: { - int charges = myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges; - PrintSBookStr(out, line1, fmt::format(ngettext("Staff ({:d} charge)", "Staff ({:d} charges)", charges), charges)); - } break; - default: { - int mana = GetManaAmount(myPlayer, sn) >> 6; - if (sn != SPL_BONESPIRIT) { - int min; - int max; - GetDamageAmt(sn, &min, &max); - if (min != -1) { - if (sn == SPL_HEAL || sn == SPL_HEALOTHER) { - PrintSBookStr(out, line1, fmt::format(_(/* TRANSLATORS: UI constrains, keep short please.*/ "Heals: {:d} - {:d}"), min, max), UiFlags::AlignRight); - } else { - PrintSBookStr(out, line1, fmt::format(_(/* TRANSLATORS: UI constrains, keep short please.*/ "Damage: {:d} - {:d}"), min, max), UiFlags::AlignRight); - } - } - } else { - PrintSBookStr(out, line1, _(/* TRANSLATORS: UI constrains, keep short please.*/ "Dmg: 1/3 target hp"), UiFlags::AlignRight); - } - PrintSBookStr(out, line1, fmt::format(pgettext(/* TRANSLATORS: UI constrains, keep short please.*/ "spellbook", "Mana: {:d}"), mana)); - int lvl = std::max(myPlayer._pSplLvl[sn] + myPlayer._pISplLvlAdd, 0); - if (lvl == 0) { - PrintSBookStr(out, line0, _("Level 0 - Unusable"), UiFlags::AlignRight); - } else { - PrintSBookStr(out, line0, fmt::format(pgettext(/* TRANSLATORS: UI constrains, keep short please.*/ "spellbook", "Level {:d}"), lvl), UiFlags::AlignRight); - } - } break; - } - } - yp += SpellBookDescriptionHeight; - } -} - -void CheckSBook() -{ - Rectangle iconArea = { GetPanelPosition(UiPanels::Spell, { 11, 18 }), { 48 - 11, 314 - 18 } }; - Rectangle tabArea = { GetPanelPosition(UiPanels::Spell, { 7, 320 }), { 311 - 7, 349 - 320 } }; - if (iconArea.Contains(MousePosition)) { - spell_id sn = SpellPages[sbooktab][(MousePosition.y - GetRightPanel().position.y - 18) / 43]; - auto &myPlayer = Players[MyPlayerId]; - uint64_t spl = myPlayer._pMemSpells | myPlayer._pISpells | myPlayer._pAblSpells; - if (sn != SPL_INVALID && (spl & GetSpellBitmask(sn)) != 0) { - spell_type st = RSPLTYPE_SPELL; - if ((myPlayer._pISpells & GetSpellBitmask(sn)) != 0) { - st = RSPLTYPE_CHARGES; - } - if ((myPlayer._pAblSpells & GetSpellBitmask(sn)) != 0) { - st = RSPLTYPE_SKILL; - } - myPlayer._pRSpell = sn; - myPlayer._pRSplType = st; - force_redraw = 255; - } - } - if (tabArea.Contains(MousePosition)) { - sbooktab = (MousePosition.x - (GetRightPanel().position.x + 7)) / (gbIsHellfire ? 61 : 76); - } -} - void DrawGoldSplit(const Surface &out, int amount) { const int dialogX = 30; diff --git a/Source/control.h b/Source/control.h index 79d6ad9cd..009bb0b95 100644 --- a/Source/control.h +++ b/Source/control.h @@ -29,13 +29,6 @@ namespace devilution { #define SPANEL_WIDTH 320 #define SPANEL_HEIGHT 352 -struct SpellListItem { - Point location; - spell_type type; - spell_id id; - bool isSelected; -}; - extern bool drawhpflag; extern bool dropGoldFlag; extern bool chrbtn[4]; @@ -176,8 +169,6 @@ void ReleaseChrBtns(bool addAllStatPoints); void DrawDurIcon(const Surface &out); void RedBack(const Surface &out); void DrawSpellBook(const Surface &out); -std::vector GetSpellListItems(); -void CheckSBook(); void DrawGoldSplit(const Surface &out, int amount); void control_drop_gold(char vkey); void DrawTalkPan(const Surface &out); diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index 74b0a41fb..8fff16a27 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -19,6 +19,7 @@ #include "items.h" #include "minitext.h" #include "missiles.h" +#include "panels/spell_list.hpp" #include "stores.h" #include "towners.h" #include "trigs.h" diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 8aee0e8fb..1b508ce9e 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -37,8 +37,8 @@ #include "error.h" #include "gamemenu.h" #include "gmenu.h" -#include "hwcursor.hpp" #include "help.h" +#include "hwcursor.hpp" #include "init.h" #include "lighting.h" #include "loadsave.h" @@ -50,6 +50,7 @@ #include "nthread.h" #include "objects.h" #include "options.h" +#include "panels/spell_book.hpp" #include "pfile.h" #include "plrmsg.h" #include "qol/common.h" diff --git a/Source/panels/spell_book.cpp b/Source/panels/spell_book.cpp new file mode 100644 index 000000000..a47003ecd --- /dev/null +++ b/Source/panels/spell_book.cpp @@ -0,0 +1,212 @@ +#include "panels/spell_book.hpp" + +#include "control.h" +#include "engine/cel_sprite.hpp" +#include "engine/load_cel.hpp" +#include "engine/rectangle.hpp" +#include "engine/render/cel_render.hpp" +#include "engine/render/text_render.hpp" +#include "init.h" +#include "missiles.h" +#include "panels/spell_icons.hpp" +#include "player.h" +#include "spelldat.h" +#include "utils/language.h" +#include "utils/stdcompat/optional.hpp" + +#define SPLICONLAST (gbIsHellfire ? 52 : 43) + +namespace devilution { +namespace { + +std::optional pSBkBtnCel; +std::optional pSBkIconCels; +std::optional pSpellBkCel; + +/** Maps from spellbook page number and position to spell_id. */ +spell_id SpellPages[6][7] = { + { SPL_NULL, SPL_FIREBOLT, SPL_CBOLT, SPL_HBOLT, SPL_HEAL, SPL_HEALOTHER, SPL_FLAME }, + { SPL_RESURRECT, SPL_FIREWALL, SPL_TELEKINESIS, SPL_LIGHTNING, SPL_TOWN, SPL_FLASH, SPL_STONE }, + { SPL_RNDTELEPORT, SPL_MANASHIELD, SPL_ELEMENT, SPL_FIREBALL, SPL_WAVE, SPL_CHAIN, SPL_GUARDIAN }, + { SPL_NOVA, SPL_GOLEM, SPL_TELEPORT, SPL_APOCA, SPL_BONESPIRIT, SPL_FLARE, SPL_ETHEREALIZE }, + { SPL_LIGHTWALL, SPL_IMMOLAT, SPL_WARP, SPL_REFLECT, SPL_BERSERK, SPL_FIRERING, SPL_SEARCH }, + { SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID } +}; + +constexpr int SpellBookDescriptionWidth = 250; +constexpr int SpellBookDescriptionHeight = 43; +constexpr int SpellBookDescriptionPaddingLeft = 2; +constexpr int SpellBookDescriptionPaddingRight = 2; + +void PrintSBookStr(const Surface &out, Point position, string_view text, UiFlags flags = UiFlags::None) +{ + DrawString(out, text, + { GetPanelPosition(UiPanels::Spell, { SPLICONLENGTH + SpellBookDescriptionPaddingLeft + position.x, position.y }), + { SpellBookDescriptionWidth - SpellBookDescriptionPaddingLeft - SpellBookDescriptionPaddingRight, 0 } }, + UiFlags::ColorWhite | flags); +} + +spell_type GetSBookTrans(spell_id ii, bool townok) +{ + auto &player = Players[MyPlayerId]; + if ((player._pClass == HeroClass::Monk) && (ii == SPL_SEARCH)) + return RSPLTYPE_SKILL; + spell_type st = RSPLTYPE_SPELL; + if ((player._pISpells & GetSpellBitmask(ii)) != 0) { + st = RSPLTYPE_CHARGES; + } + if ((player._pAblSpells & GetSpellBitmask(ii)) != 0) { + st = RSPLTYPE_SKILL; + } + if (st == RSPLTYPE_SPELL) { + if (CheckSpell(MyPlayerId, ii, st, true) != SpellCheckResult::Success) { + st = RSPLTYPE_INVALID; + } + if ((char)(player._pSplLvl[ii] + player._pISplLvlAdd) <= 0) { + st = RSPLTYPE_INVALID; + } + } + if (townok && currlevel == 0 && st != RSPLTYPE_INVALID && !spelldata[ii].sTownSpell) { + st = RSPLTYPE_INVALID; + } + + return st; +} + +} // namespace + +void InitSpellBook() +{ + pSpellBkCel = LoadCel("Data\\SpellBk.CEL", SPANEL_WIDTH); + + if (gbIsHellfire) { + static const int SBkBtnHellfireWidths[] = { 0, 61, 61, 61, 61, 61, 76 }; + pSBkBtnCel = LoadCel("Data\\SpellBkB.CEL", SBkBtnHellfireWidths); + } else { + pSBkBtnCel = LoadCel("Data\\SpellBkB.CEL", 76); + } + pSBkIconCels = LoadCel("Data\\SpellI2.CEL", 37); + + Player &player = Players[MyPlayerId]; + if (player._pClass == HeroClass::Warrior) { + SpellPages[0][0] = SPL_REPAIR; + } else if (player._pClass == HeroClass::Rogue) { + SpellPages[0][0] = SPL_DISARM; + } else if (player._pClass == HeroClass::Sorcerer) { + SpellPages[0][0] = SPL_RECHARGE; + } else if (player._pClass == HeroClass::Monk) { + SpellPages[0][0] = SPL_SEARCH; + } else if (player._pClass == HeroClass::Bard) { + SpellPages[0][0] = SPL_IDENTIFY; + } else if (player._pClass == HeroClass::Barbarian) { + SpellPages[0][0] = SPL_BLODBOIL; + } +} + +void FreeSpellBook() +{ + pSpellBkCel = std::nullopt; + pSBkBtnCel = std::nullopt; + pSBkIconCels = std::nullopt; +} + +void DrawSpellBook(const Surface &out) +{ + CelDrawTo(out, GetPanelPosition(UiPanels::Spell, { 0, 351 }), *pSpellBkCel, 1); + if (gbIsHellfire && sbooktab < 5) { + CelDrawTo(out, GetPanelPosition(UiPanels::Spell, { 61 * sbooktab + 7, 348 }), *pSBkBtnCel, sbooktab + 1); + } else { + // BUGFIX: rendering of page 3 and page 4 buttons are both off-by-one pixel (fixed). + int sx = 76 * sbooktab + 7; + if (sbooktab == 2 || sbooktab == 3) { + sx++; + } + CelDrawTo(out, GetPanelPosition(UiPanels::Spell, { sx, 348 }), *pSBkBtnCel, sbooktab + 1); + } + auto &player = Players[MyPlayerId]; + uint64_t spl = player._pMemSpells | player._pISpells | player._pAblSpells; + + const int lineHeight = 18; + + int yp = 12; + const int textPaddingTop = 7; + for (int i = 1; i < 8; i++) { + spell_id sn = SpellPages[sbooktab][i - 1]; + if (sn != SPL_INVALID && (spl & GetSpellBitmask(sn)) != 0) { + spell_type st = GetSBookTrans(sn, true); + SetSpellTrans(st); + const Point spellCellPosition = GetPanelPosition(UiPanels::Spell, { 11, yp + SpellBookDescriptionHeight }); + DrawSpellCel(out, spellCellPosition, *pSBkIconCels, SpellITbl[sn]); + if (sn == player._pRSpell && st == player._pRSplType) { + SetSpellTrans(RSPLTYPE_SKILL); + DrawSpellCel(out, spellCellPosition, *pSBkIconCels, SPLICONLAST); + } + + const Point line0 { 0, yp + textPaddingTop }; + const Point line1 { 0, yp + textPaddingTop + lineHeight }; + PrintSBookStr(out, line0, pgettext("spell", spelldata[sn].sNameText)); + switch (GetSBookTrans(sn, false)) { + case RSPLTYPE_SKILL: + PrintSBookStr(out, line1, _("Skill")); + break; + case RSPLTYPE_CHARGES: { + int charges = player.InvBody[INVLOC_HAND_LEFT]._iCharges; + PrintSBookStr(out, line1, fmt::format(ngettext("Staff ({:d} charge)", "Staff ({:d} charges)", charges), charges)); + } break; + default: { + int mana = GetManaAmount(player, sn) >> 6; + if (sn != SPL_BONESPIRIT) { + int min; + int max; + GetDamageAmt(sn, &min, &max); + if (min != -1) { + if (sn == SPL_HEAL || sn == SPL_HEALOTHER) { + PrintSBookStr(out, line1, fmt::format(_(/* TRANSLATORS: UI constrains, keep short please.*/ "Heals: {:d} - {:d}"), min, max), UiFlags::AlignRight); + } else { + PrintSBookStr(out, line1, fmt::format(_(/* TRANSLATORS: UI constrains, keep short please.*/ "Damage: {:d} - {:d}"), min, max), UiFlags::AlignRight); + } + } + } else { + PrintSBookStr(out, line1, _(/* TRANSLATORS: UI constrains, keep short please.*/ "Dmg: 1/3 target hp"), UiFlags::AlignRight); + } + PrintSBookStr(out, line1, fmt::format(pgettext(/* TRANSLATORS: UI constrains, keep short please.*/ "spellbook", "Mana: {:d}"), mana)); + int lvl = std::max(player._pSplLvl[sn] + player._pISplLvlAdd, 0); + if (lvl == 0) { + PrintSBookStr(out, line0, _("Level 0 - Unusable"), UiFlags::AlignRight); + } else { + PrintSBookStr(out, line0, fmt::format(pgettext(/* TRANSLATORS: UI constrains, keep short please.*/ "spellbook", "Level {:d}"), lvl), UiFlags::AlignRight); + } + } break; + } + } + yp += SpellBookDescriptionHeight; + } +} + +void CheckSBook() +{ + Rectangle iconArea = { GetPanelPosition(UiPanels::Spell, { 11, 18 }), { 48 - 11, 314 - 18 } }; + Rectangle tabArea = { GetPanelPosition(UiPanels::Spell, { 7, 320 }), { 311 - 7, 349 - 320 } }; + if (iconArea.Contains(MousePosition)) { + spell_id sn = SpellPages[sbooktab][(MousePosition.y - GetRightPanel().position.y - 18) / 43]; + auto &player = Players[MyPlayerId]; + uint64_t spl = player._pMemSpells | player._pISpells | player._pAblSpells; + if (sn != SPL_INVALID && (spl & GetSpellBitmask(sn)) != 0) { + spell_type st = RSPLTYPE_SPELL; + if ((player._pISpells & GetSpellBitmask(sn)) != 0) { + st = RSPLTYPE_CHARGES; + } + if ((player._pAblSpells & GetSpellBitmask(sn)) != 0) { + st = RSPLTYPE_SKILL; + } + player._pRSpell = sn; + player._pRSplType = st; + force_redraw = 255; + } + } + if (tabArea.Contains(MousePosition)) { + sbooktab = (MousePosition.x - (GetRightPanel().position.x + 7)) / (gbIsHellfire ? 61 : 76); + } +} + +} // namespace devilution diff --git a/Source/panels/spell_book.hpp b/Source/panels/spell_book.hpp new file mode 100644 index 000000000..2a9aeaa39 --- /dev/null +++ b/Source/panels/spell_book.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "engine/surface.hpp" + +namespace devilution { + +void InitSpellBook(); +void FreeSpellBook(); +void CheckSBook(); +void DrawSpellBook(const Surface &out); + +} // namespace devilution diff --git a/Source/panels/spell_icons.cpp b/Source/panels/spell_icons.cpp new file mode 100644 index 000000000..b5eb20ebf --- /dev/null +++ b/Source/panels/spell_icons.cpp @@ -0,0 +1,152 @@ +#include "panels/spell_icons.hpp" + +#include "engine/load_cel.hpp" +#include "engine/render/cel_render.hpp" +#include "init.h" +#include "palette.h" +#include "utils/stdcompat/optional.hpp" + +namespace devilution { + +namespace { +std::optional pSpellCels; +uint8_t SplTransTbl[256]; +} // namespace + +const char SpellITbl[] = { + 27, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 28, + 13, + 12, + 18, + 16, + 14, + 18, + 19, + 11, + 20, + 15, + 21, + 23, + 24, + 25, + 22, + 26, + 29, + 37, + 38, + 39, + 42, + 41, + 40, + 10, + 36, + 30, + 51, + 51, + 50, + 46, + 47, + 43, + 45, + 48, + 49, + 44, + 35, + 35, + 35, + 35, + 35, +}; + +void LoadSpellIcons() +{ + if (!gbIsHellfire) + pSpellCels = LoadCel("CtrlPan\\SpelIcon.CEL", SPLICONLENGTH); + else + pSpellCels = LoadCel("Data\\SpelIcon.CEL", SPLICONLENGTH); + SetSpellTrans(RSPLTYPE_SKILL); +} + +void FreeSpellIcons() +{ + pSpellCels = std::nullopt; +} + +void DrawSpellCel(const Surface &out, Point position, int nCel) +{ + DrawSpellCel(out, position, *pSpellCels, nCel); +} + +void DrawSpellCel(const Surface &out, Point position, const CelSprite &sprite, int nCel) +{ + CelDrawLightTo(out, position, sprite, nCel, SplTransTbl); +} + +void SetSpellTrans(spell_type t) +{ + if (t == RSPLTYPE_SKILL) { + for (int i = 0; i < 128; i++) + SplTransTbl[i] = i; + } + for (int i = 128; i < 256; i++) + SplTransTbl[i] = i; + SplTransTbl[255] = 0; + + switch (t) { + case RSPLTYPE_SPELL: + SplTransTbl[PAL8_YELLOW] = PAL16_BLUE + 1; + SplTransTbl[PAL8_YELLOW + 1] = PAL16_BLUE + 3; + SplTransTbl[PAL8_YELLOW + 2] = PAL16_BLUE + 5; + for (int i = PAL16_BLUE; i < PAL16_BLUE + 16; i++) { + SplTransTbl[PAL16_BEIGE - PAL16_BLUE + i] = i; + SplTransTbl[PAL16_YELLOW - PAL16_BLUE + i] = i; + SplTransTbl[PAL16_ORANGE - PAL16_BLUE + i] = i; + } + break; + case RSPLTYPE_SCROLL: + SplTransTbl[PAL8_YELLOW] = PAL16_BEIGE + 1; + SplTransTbl[PAL8_YELLOW + 1] = PAL16_BEIGE + 3; + SplTransTbl[PAL8_YELLOW + 2] = PAL16_BEIGE + 5; + for (int i = PAL16_BEIGE; i < PAL16_BEIGE + 16; i++) { + SplTransTbl[PAL16_YELLOW - PAL16_BEIGE + i] = i; + SplTransTbl[PAL16_ORANGE - PAL16_BEIGE + i] = i; + } + break; + case RSPLTYPE_CHARGES: + SplTransTbl[PAL8_YELLOW] = PAL16_ORANGE + 1; + SplTransTbl[PAL8_YELLOW + 1] = PAL16_ORANGE + 3; + SplTransTbl[PAL8_YELLOW + 2] = PAL16_ORANGE + 5; + for (int i = PAL16_ORANGE; i < PAL16_ORANGE + 16; i++) { + SplTransTbl[PAL16_BEIGE - PAL16_ORANGE + i] = i; + SplTransTbl[PAL16_YELLOW - PAL16_ORANGE + i] = i; + } + break; + case RSPLTYPE_INVALID: + SplTransTbl[PAL8_YELLOW] = PAL16_GRAY + 1; + SplTransTbl[PAL8_YELLOW + 1] = PAL16_GRAY + 3; + SplTransTbl[PAL8_YELLOW + 2] = PAL16_GRAY + 5; + for (int i = PAL16_GRAY; i < PAL16_GRAY + 15; i++) { + SplTransTbl[PAL16_BEIGE - PAL16_GRAY + i] = i; + SplTransTbl[PAL16_YELLOW - PAL16_GRAY + i] = i; + SplTransTbl[PAL16_ORANGE - PAL16_GRAY + i] = i; + } + SplTransTbl[PAL16_BEIGE + 15] = 0; + SplTransTbl[PAL16_YELLOW + 15] = 0; + SplTransTbl[PAL16_ORANGE + 15] = 0; + break; + case RSPLTYPE_SKILL: + break; + } +} + +} // namespace devilution diff --git a/Source/panels/spell_icons.hpp b/Source/panels/spell_icons.hpp new file mode 100644 index 000000000..e2ecc97da --- /dev/null +++ b/Source/panels/spell_icons.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "engine/cel_sprite.hpp" +#include "engine/point.hpp" +#include "engine/surface.hpp" +#include "spelldat.h" + +#define SPLICONLENGTH 56 + +namespace devilution { + +/** Maps from spell_id to spelicon.cel frame number. */ +extern const char SpellITbl[]; + +/** + * Draw spell icon onto the given buffer. + * @param out Output buffer. + * @param position Buffer coordinates. + * @param nCel Index of the cel frame to draw. 0 based. + */ +void DrawSpellCel(const Surface &out, Point position, int nCel); + +/** + * Draw spell icon onto the given buffer. + * @param out Output buffer. + * @param position Buffer coordinates. + * @param sprite Icons sprite sheet. + * @param nCel Index of the cel frame to draw. 0 based. + */ +void DrawSpellCel(const Surface &out, Point position, const CelSprite &sprite, int nCel); + +void SetSpellTrans(spell_type t); + +void LoadSpellIcons(); +void FreeSpellIcons(); + +} // namespace devilution diff --git a/Source/panels/spell_list.cpp b/Source/panels/spell_list.cpp new file mode 100644 index 000000000..222776286 --- /dev/null +++ b/Source/panels/spell_list.cpp @@ -0,0 +1,372 @@ +#include "panels/spell_list.hpp" + +#include "control.h" +#include "controls/keymapper.hpp" +#include "engine.h" +#include "engine/render/text_render.hpp" +#include "inv_iterators.hpp" +#include "palette.h" +#include "panels/spell_icons.hpp" +#include "player.h" +#include "spells.h" +#include "utils/language.h" + +#define SPLROWICONLS 10 + +namespace devilution { + +extern std::array quickSpellActionIndexes; + +namespace { + +void PrintSBookSpellType(const Surface &out, Point position, const std::string &text, uint8_t rectColorIndex) +{ + Point rect { position }; + rect += Displacement { 0, -SPLICONLENGTH + 1 }; + + // Top + DrawHorizontalLine(out, rect, SPLICONLENGTH, rectColorIndex); + DrawHorizontalLine(out, rect + Displacement { 0, 1 }, SPLICONLENGTH, rectColorIndex); + + // Bottom + DrawHorizontalLine(out, rect + Displacement { 0, SPLICONLENGTH - 2 }, SPLICONLENGTH, rectColorIndex); + DrawHorizontalLine(out, rect + Displacement { 0, SPLICONLENGTH - 1 }, SPLICONLENGTH, rectColorIndex); + + // Left Side + DrawVerticalLine(out, rect, SPLICONLENGTH, rectColorIndex); + DrawVerticalLine(out, rect + Displacement { 1, 0 }, SPLICONLENGTH, rectColorIndex); + + // Right Side + DrawVerticalLine(out, rect + Displacement { SPLICONLENGTH - 2, 0 }, SPLICONLENGTH, rectColorIndex); + DrawVerticalLine(out, rect + Displacement { SPLICONLENGTH - 1, 0 }, SPLICONLENGTH, rectColorIndex); + + // Align the spell type text with bottom of spell icon + position += Displacement { SPLICONLENGTH / 2 - GetLineWidth(text.c_str()) / 2, (IsSmallFontTall() ? -19 : -15) }; + + // Draw a drop shadow below and to the left of the text + DrawString(out, text, position + Displacement { -1, 1 }, UiFlags::ColorBlack); + DrawString(out, text, position + Displacement { -1, -1 }, UiFlags::ColorBlack); + DrawString(out, text, position + Displacement { 1, -1 }, UiFlags::ColorBlack); + // Then draw the text over the top + DrawString(out, text, position, UiFlags::ColorWhite); +} + +void PrintSBookHotkey(const Surface &out, Point position, const std::string &text) +{ + // Align the hot key text with the top-right corner of the spell icon + position += Displacement { SPLICONLENGTH - (GetLineWidth(text.c_str()) + 5), 5 - SPLICONLENGTH }; + + // Draw a drop shadow below and to the left of the text + DrawString(out, text, position + Displacement { -1, 1 }, UiFlags::ColorBlack); + // Then draw the text over the top + DrawString(out, text, position, UiFlags::ColorWhite); +} + +bool GetSpellListSelection(spell_id &pSpell, spell_type &pSplType) +{ + pSpell = spell_id::SPL_INVALID; + pSplType = spell_type::RSPLTYPE_INVALID; + auto &myPlayer = Players[MyPlayerId]; + + for (auto &spellListItem : GetSpellListItems()) { + if (spellListItem.isSelected) { + pSpell = spellListItem.id; + pSplType = spellListItem.type; + if (myPlayer._pClass == HeroClass::Monk && spellListItem.id == SPL_SEARCH) + pSplType = RSPLTYPE_SKILL; + return true; + } + } + + return false; +} + +} // namespace + +void DrawSpell(const Surface &out) +{ + auto &myPlayer = Players[MyPlayerId]; + spell_id spl = myPlayer._pRSpell; + spell_type st = myPlayer._pRSplType; + + // BUGFIX: Move the next line into the if statement to avoid OOB (SPL_INVALID is -1) (fixed) + if (st == RSPLTYPE_SPELL && spl != SPL_INVALID) { + int tlvl = myPlayer._pISplLvlAdd + myPlayer._pSplLvl[spl]; + if (CheckSpell(MyPlayerId, spl, st, true) != SpellCheckResult::Success) + st = RSPLTYPE_INVALID; + if (tlvl <= 0) + st = RSPLTYPE_INVALID; + } + if (currlevel == 0 && st != RSPLTYPE_INVALID && !spelldata[spl].sTownSpell) + st = RSPLTYPE_INVALID; + SetSpellTrans(st); + const int nCel = (spl != SPL_INVALID) ? SpellITbl[spl] : 27; + const Point position { PANEL_X + 565, PANEL_Y + 119 }; + DrawSpellCel(out, position, nCel); +} + +void DrawSpellList(const Surface &out) +{ + infostr[0] = '\0'; + ClearPanel(); + + auto &myPlayer = Players[MyPlayerId]; + + for (auto &spellListItem : GetSpellListItems()) { + const spell_id spellId = spellListItem.id; + spell_type transType = spellListItem.type; + int spellLevel = 0; + const SpellData &spellDataItem = spelldata[static_cast(spellListItem.id)]; + if (currlevel == 0 && !spellDataItem.sTownSpell) { + transType = RSPLTYPE_INVALID; + } + if (spellListItem.type == RSPLTYPE_SPELL) { + spellLevel = std::max(myPlayer._pISplLvlAdd + myPlayer._pSplLvl[spellListItem.id], 0); + if (spellLevel == 0) + transType = RSPLTYPE_INVALID; + } + + SetSpellTrans(transType); + DrawSpellCel(out, spellListItem.location, SpellITbl[static_cast(spellId)]); + + if (!spellListItem.isSelected) + continue; + + uint8_t spellColor = PAL16_GRAY + 5; + + switch (spellListItem.type) { + case RSPLTYPE_SKILL: + spellColor = PAL16_YELLOW - 46; + PrintSBookSpellType(out, spellListItem.location, _("Skill"), spellColor); + strcpy(infostr, fmt::format(_("{:s} Skill"), pgettext("spell", spellDataItem.sSkillText)).c_str()); + break; + case RSPLTYPE_SPELL: + if (myPlayer.plrlevel != 0) { + spellColor = PAL16_BLUE + 5; + } + PrintSBookSpellType(out, spellListItem.location, _("Spell"), spellColor); + strcpy(infostr, fmt::format(_("{:s} Spell"), pgettext("spell", spellDataItem.sNameText)).c_str()); + if (spellId == SPL_HBOLT) { + strcpy(tempstr, _("Damages undead only")); + AddPanelString(tempstr); + } + if (spellLevel == 0) + strcpy(tempstr, _("Spell Level 0 - Unusable")); + else + strcpy(tempstr, fmt::format(_("Spell Level {:d}"), spellLevel).c_str()); + AddPanelString(tempstr); + break; + case RSPLTYPE_SCROLL: { + if (myPlayer.plrlevel != 0) { + spellColor = PAL16_RED - 59; + } + PrintSBookSpellType(out, spellListItem.location, _("Scroll"), spellColor); + strcpy(infostr, fmt::format(_("Scroll of {:s}"), pgettext("spell", spellDataItem.sNameText)).c_str()); + const InventoryAndBeltPlayerItemsRange items { myPlayer }; + const int scrollCount = std::count_if(items.begin(), items.end(), [spellId](const Item &item) { + return item.IsScrollOf(spellId); + }); + strcpy(tempstr, fmt::format(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount), scrollCount).c_str()); + AddPanelString(tempstr); + } break; + case RSPLTYPE_CHARGES: { + if (myPlayer.plrlevel != 0) { + spellColor = PAL16_ORANGE + 5; + } + PrintSBookSpellType(out, spellListItem.location, _("Staff"), spellColor); + strcpy(infostr, fmt::format(_("Staff of {:s}"), pgettext("spell", spellDataItem.sNameText)).c_str()); + int charges = myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges; + strcpy(tempstr, fmt::format(ngettext("{:d} Charge", "{:d} Charges", charges), charges).c_str()); + AddPanelString(tempstr); + } break; + case RSPLTYPE_INVALID: + break; + } + for (int t = 0; t < 4; t++) { + if (myPlayer._pSplHotKey[t] == spellId && myPlayer._pSplTHotKey[t] == spellListItem.type) { + auto hotkeyName = keymapper.KeyNameForAction(quickSpellActionIndexes[t]); + PrintSBookHotkey(out, spellListItem.location, hotkeyName); + strcpy(tempstr, fmt::format(_("Spell Hotkey {:s}"), hotkeyName.c_str()).c_str()); + AddPanelString(tempstr); + } + } + } +} + +std::vector GetSpellListItems() +{ + std::vector spellListItems; + + uint64_t mask; + + int x = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + int y = PANEL_Y - 17; + + for (int i = RSPLTYPE_SKILL; i < RSPLTYPE_INVALID; i++) { + auto &myPlayer = Players[MyPlayerId]; + switch ((spell_type)i) { + case RSPLTYPE_SKILL: + mask = myPlayer._pAblSpells; + break; + case RSPLTYPE_SPELL: + mask = myPlayer._pMemSpells; + break; + case RSPLTYPE_SCROLL: + mask = myPlayer._pScrlSpells; + break; + case RSPLTYPE_CHARGES: + mask = myPlayer._pISpells; + break; + case RSPLTYPE_INVALID: + break; + } + int8_t j = SPL_FIREBOLT; + for (uint64_t spl = 1; j < MAX_SPELLS; spl <<= 1, j++) { + if ((mask & spl) == 0) + continue; + int lx = x; + int ly = y - SPLICONLENGTH; + bool isSelected = (MousePosition.x >= lx && MousePosition.x < lx + SPLICONLENGTH && MousePosition.y >= ly && MousePosition.y < ly + SPLICONLENGTH); + spellListItems.emplace_back(SpellListItem { { x, y }, (spell_type)i, (spell_id)j, isSelected }); + x -= SPLICONLENGTH; + if (x == PANEL_X + 12 - SPLICONLENGTH) { + x = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + y -= SPLICONLENGTH; + } + } + if (mask != 0 && x != PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS) + x -= SPLICONLENGTH; + if (x == PANEL_X + 12 - SPLICONLENGTH) { + x = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + y -= SPLICONLENGTH; + } + } + + return spellListItems; +} + +void SetSpell() +{ + spell_id pSpell; + spell_type pSplType; + + spselflag = false; + if (!GetSpellListSelection(pSpell, pSplType)) { + return; + } + + ClearPanel(); + + auto &myPlayer = Players[MyPlayerId]; + myPlayer._pRSpell = pSpell; + myPlayer._pRSplType = pSplType; + + force_redraw = 255; +} + +void SetSpeedSpell(int slot) +{ + spell_id pSpell; + spell_type pSplType; + + if (!GetSpellListSelection(pSpell, pSplType)) { + return; + } + auto &myPlayer = Players[MyPlayerId]; + for (int i = 0; i < 4; ++i) { + if (myPlayer._pSplHotKey[i] == pSpell && myPlayer._pSplTHotKey[i] == pSplType) + myPlayer._pSplHotKey[i] = SPL_INVALID; + } + myPlayer._pSplHotKey[slot] = pSpell; + myPlayer._pSplTHotKey[slot] = pSplType; +} + +void ToggleSpell(int slot) +{ + uint64_t spells; + + auto &myPlayer = Players[MyPlayerId]; + + if (myPlayer._pSplHotKey[slot] == SPL_INVALID) { + return; + } + + switch (myPlayer._pSplTHotKey[slot]) { + case RSPLTYPE_SKILL: + spells = myPlayer._pAblSpells; + break; + case RSPLTYPE_SPELL: + spells = myPlayer._pMemSpells; + break; + case RSPLTYPE_SCROLL: + spells = myPlayer._pScrlSpells; + break; + case RSPLTYPE_CHARGES: + spells = myPlayer._pISpells; + break; + case RSPLTYPE_INVALID: + return; + } + + if ((spells & GetSpellBitmask(myPlayer._pSplHotKey[slot])) != 0) { + myPlayer._pRSpell = myPlayer._pSplHotKey[slot]; + myPlayer._pRSplType = myPlayer._pSplTHotKey[slot]; + force_redraw = 255; + } +} + +void DoSpeedBook() +{ + spselflag = true; + int xo = PANEL_X + 12 + SPLICONLENGTH * 10; + int yo = PANEL_Y - 17; + int x = xo + SPLICONLENGTH / 2; + int y = yo - SPLICONLENGTH / 2; + + auto &myPlayer = Players[MyPlayerId]; + + if (myPlayer._pRSpell != SPL_INVALID) { + for (int i = RSPLTYPE_SKILL; i <= RSPLTYPE_CHARGES; i++) { + uint64_t spells; + switch (i) { + case RSPLTYPE_SKILL: + spells = myPlayer._pAblSpells; + break; + case RSPLTYPE_SPELL: + spells = myPlayer._pMemSpells; + break; + case RSPLTYPE_SCROLL: + spells = myPlayer._pScrlSpells; + break; + case RSPLTYPE_CHARGES: + spells = myPlayer._pISpells; + break; + } + uint64_t spell = 1; + for (int j = 1; j < MAX_SPELLS; j++) { + if ((spell & spells) != 0) { + if (j == myPlayer._pRSpell && i == myPlayer._pRSplType) { + x = xo + SPLICONLENGTH / 2; + y = yo - SPLICONLENGTH / 2; + } + xo -= SPLICONLENGTH; + if (xo == PANEL_X + 12 - SPLICONLENGTH) { + xo = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + yo -= SPLICONLENGTH; + } + } + spell <<= 1ULL; + } + if (spells != 0 && xo != PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS) + xo -= SPLICONLENGTH; + if (xo == PANEL_X + 12 - SPLICONLENGTH) { + xo = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + yo -= SPLICONLENGTH; + } + } + } + + SetCursorPos({ x, y }); +} + +} // namespace devilution diff --git a/Source/panels/spell_list.hpp b/Source/panels/spell_list.hpp new file mode 100644 index 000000000..a2209e458 --- /dev/null +++ b/Source/panels/spell_list.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "engine/point.hpp" +#include "engine/surface.hpp" +#include "spelldat.h" + +namespace devilution { + +struct SpellListItem { + Point location; + spell_type type; + spell_id id; + bool isSelected; +}; + +void DrawSpell(const Surface &out); +void DrawSpellList(const Surface &out); +std::vector GetSpellListItems(); +void SetSpell(); +void SetSpeedSpell(int slot); +void ToggleSpell(int slot); +void DoSpeedBook(); + +} // namespace devilution diff --git a/Source/spell_book.cpp b/Source/spell_book.cpp new file mode 100644 index 000000000..a47003ecd --- /dev/null +++ b/Source/spell_book.cpp @@ -0,0 +1,212 @@ +#include "panels/spell_book.hpp" + +#include "control.h" +#include "engine/cel_sprite.hpp" +#include "engine/load_cel.hpp" +#include "engine/rectangle.hpp" +#include "engine/render/cel_render.hpp" +#include "engine/render/text_render.hpp" +#include "init.h" +#include "missiles.h" +#include "panels/spell_icons.hpp" +#include "player.h" +#include "spelldat.h" +#include "utils/language.h" +#include "utils/stdcompat/optional.hpp" + +#define SPLICONLAST (gbIsHellfire ? 52 : 43) + +namespace devilution { +namespace { + +std::optional pSBkBtnCel; +std::optional pSBkIconCels; +std::optional pSpellBkCel; + +/** Maps from spellbook page number and position to spell_id. */ +spell_id SpellPages[6][7] = { + { SPL_NULL, SPL_FIREBOLT, SPL_CBOLT, SPL_HBOLT, SPL_HEAL, SPL_HEALOTHER, SPL_FLAME }, + { SPL_RESURRECT, SPL_FIREWALL, SPL_TELEKINESIS, SPL_LIGHTNING, SPL_TOWN, SPL_FLASH, SPL_STONE }, + { SPL_RNDTELEPORT, SPL_MANASHIELD, SPL_ELEMENT, SPL_FIREBALL, SPL_WAVE, SPL_CHAIN, SPL_GUARDIAN }, + { SPL_NOVA, SPL_GOLEM, SPL_TELEPORT, SPL_APOCA, SPL_BONESPIRIT, SPL_FLARE, SPL_ETHEREALIZE }, + { SPL_LIGHTWALL, SPL_IMMOLAT, SPL_WARP, SPL_REFLECT, SPL_BERSERK, SPL_FIRERING, SPL_SEARCH }, + { SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID, SPL_INVALID } +}; + +constexpr int SpellBookDescriptionWidth = 250; +constexpr int SpellBookDescriptionHeight = 43; +constexpr int SpellBookDescriptionPaddingLeft = 2; +constexpr int SpellBookDescriptionPaddingRight = 2; + +void PrintSBookStr(const Surface &out, Point position, string_view text, UiFlags flags = UiFlags::None) +{ + DrawString(out, text, + { GetPanelPosition(UiPanels::Spell, { SPLICONLENGTH + SpellBookDescriptionPaddingLeft + position.x, position.y }), + { SpellBookDescriptionWidth - SpellBookDescriptionPaddingLeft - SpellBookDescriptionPaddingRight, 0 } }, + UiFlags::ColorWhite | flags); +} + +spell_type GetSBookTrans(spell_id ii, bool townok) +{ + auto &player = Players[MyPlayerId]; + if ((player._pClass == HeroClass::Monk) && (ii == SPL_SEARCH)) + return RSPLTYPE_SKILL; + spell_type st = RSPLTYPE_SPELL; + if ((player._pISpells & GetSpellBitmask(ii)) != 0) { + st = RSPLTYPE_CHARGES; + } + if ((player._pAblSpells & GetSpellBitmask(ii)) != 0) { + st = RSPLTYPE_SKILL; + } + if (st == RSPLTYPE_SPELL) { + if (CheckSpell(MyPlayerId, ii, st, true) != SpellCheckResult::Success) { + st = RSPLTYPE_INVALID; + } + if ((char)(player._pSplLvl[ii] + player._pISplLvlAdd) <= 0) { + st = RSPLTYPE_INVALID; + } + } + if (townok && currlevel == 0 && st != RSPLTYPE_INVALID && !spelldata[ii].sTownSpell) { + st = RSPLTYPE_INVALID; + } + + return st; +} + +} // namespace + +void InitSpellBook() +{ + pSpellBkCel = LoadCel("Data\\SpellBk.CEL", SPANEL_WIDTH); + + if (gbIsHellfire) { + static const int SBkBtnHellfireWidths[] = { 0, 61, 61, 61, 61, 61, 76 }; + pSBkBtnCel = LoadCel("Data\\SpellBkB.CEL", SBkBtnHellfireWidths); + } else { + pSBkBtnCel = LoadCel("Data\\SpellBkB.CEL", 76); + } + pSBkIconCels = LoadCel("Data\\SpellI2.CEL", 37); + + Player &player = Players[MyPlayerId]; + if (player._pClass == HeroClass::Warrior) { + SpellPages[0][0] = SPL_REPAIR; + } else if (player._pClass == HeroClass::Rogue) { + SpellPages[0][0] = SPL_DISARM; + } else if (player._pClass == HeroClass::Sorcerer) { + SpellPages[0][0] = SPL_RECHARGE; + } else if (player._pClass == HeroClass::Monk) { + SpellPages[0][0] = SPL_SEARCH; + } else if (player._pClass == HeroClass::Bard) { + SpellPages[0][0] = SPL_IDENTIFY; + } else if (player._pClass == HeroClass::Barbarian) { + SpellPages[0][0] = SPL_BLODBOIL; + } +} + +void FreeSpellBook() +{ + pSpellBkCel = std::nullopt; + pSBkBtnCel = std::nullopt; + pSBkIconCels = std::nullopt; +} + +void DrawSpellBook(const Surface &out) +{ + CelDrawTo(out, GetPanelPosition(UiPanels::Spell, { 0, 351 }), *pSpellBkCel, 1); + if (gbIsHellfire && sbooktab < 5) { + CelDrawTo(out, GetPanelPosition(UiPanels::Spell, { 61 * sbooktab + 7, 348 }), *pSBkBtnCel, sbooktab + 1); + } else { + // BUGFIX: rendering of page 3 and page 4 buttons are both off-by-one pixel (fixed). + int sx = 76 * sbooktab + 7; + if (sbooktab == 2 || sbooktab == 3) { + sx++; + } + CelDrawTo(out, GetPanelPosition(UiPanels::Spell, { sx, 348 }), *pSBkBtnCel, sbooktab + 1); + } + auto &player = Players[MyPlayerId]; + uint64_t spl = player._pMemSpells | player._pISpells | player._pAblSpells; + + const int lineHeight = 18; + + int yp = 12; + const int textPaddingTop = 7; + for (int i = 1; i < 8; i++) { + spell_id sn = SpellPages[sbooktab][i - 1]; + if (sn != SPL_INVALID && (spl & GetSpellBitmask(sn)) != 0) { + spell_type st = GetSBookTrans(sn, true); + SetSpellTrans(st); + const Point spellCellPosition = GetPanelPosition(UiPanels::Spell, { 11, yp + SpellBookDescriptionHeight }); + DrawSpellCel(out, spellCellPosition, *pSBkIconCels, SpellITbl[sn]); + if (sn == player._pRSpell && st == player._pRSplType) { + SetSpellTrans(RSPLTYPE_SKILL); + DrawSpellCel(out, spellCellPosition, *pSBkIconCels, SPLICONLAST); + } + + const Point line0 { 0, yp + textPaddingTop }; + const Point line1 { 0, yp + textPaddingTop + lineHeight }; + PrintSBookStr(out, line0, pgettext("spell", spelldata[sn].sNameText)); + switch (GetSBookTrans(sn, false)) { + case RSPLTYPE_SKILL: + PrintSBookStr(out, line1, _("Skill")); + break; + case RSPLTYPE_CHARGES: { + int charges = player.InvBody[INVLOC_HAND_LEFT]._iCharges; + PrintSBookStr(out, line1, fmt::format(ngettext("Staff ({:d} charge)", "Staff ({:d} charges)", charges), charges)); + } break; + default: { + int mana = GetManaAmount(player, sn) >> 6; + if (sn != SPL_BONESPIRIT) { + int min; + int max; + GetDamageAmt(sn, &min, &max); + if (min != -1) { + if (sn == SPL_HEAL || sn == SPL_HEALOTHER) { + PrintSBookStr(out, line1, fmt::format(_(/* TRANSLATORS: UI constrains, keep short please.*/ "Heals: {:d} - {:d}"), min, max), UiFlags::AlignRight); + } else { + PrintSBookStr(out, line1, fmt::format(_(/* TRANSLATORS: UI constrains, keep short please.*/ "Damage: {:d} - {:d}"), min, max), UiFlags::AlignRight); + } + } + } else { + PrintSBookStr(out, line1, _(/* TRANSLATORS: UI constrains, keep short please.*/ "Dmg: 1/3 target hp"), UiFlags::AlignRight); + } + PrintSBookStr(out, line1, fmt::format(pgettext(/* TRANSLATORS: UI constrains, keep short please.*/ "spellbook", "Mana: {:d}"), mana)); + int lvl = std::max(player._pSplLvl[sn] + player._pISplLvlAdd, 0); + if (lvl == 0) { + PrintSBookStr(out, line0, _("Level 0 - Unusable"), UiFlags::AlignRight); + } else { + PrintSBookStr(out, line0, fmt::format(pgettext(/* TRANSLATORS: UI constrains, keep short please.*/ "spellbook", "Level {:d}"), lvl), UiFlags::AlignRight); + } + } break; + } + } + yp += SpellBookDescriptionHeight; + } +} + +void CheckSBook() +{ + Rectangle iconArea = { GetPanelPosition(UiPanels::Spell, { 11, 18 }), { 48 - 11, 314 - 18 } }; + Rectangle tabArea = { GetPanelPosition(UiPanels::Spell, { 7, 320 }), { 311 - 7, 349 - 320 } }; + if (iconArea.Contains(MousePosition)) { + spell_id sn = SpellPages[sbooktab][(MousePosition.y - GetRightPanel().position.y - 18) / 43]; + auto &player = Players[MyPlayerId]; + uint64_t spl = player._pMemSpells | player._pISpells | player._pAblSpells; + if (sn != SPL_INVALID && (spl & GetSpellBitmask(sn)) != 0) { + spell_type st = RSPLTYPE_SPELL; + if ((player._pISpells & GetSpellBitmask(sn)) != 0) { + st = RSPLTYPE_CHARGES; + } + if ((player._pAblSpells & GetSpellBitmask(sn)) != 0) { + st = RSPLTYPE_SKILL; + } + player._pRSpell = sn; + player._pRSplType = st; + force_redraw = 255; + } + } + if (tabArea.Contains(MousePosition)) { + sbooktab = (MousePosition.x - (GetRightPanel().position.x + 7)) / (gbIsHellfire ? 61 : 76); + } +} + +} // namespace devilution diff --git a/Source/spell_icons.cpp b/Source/spell_icons.cpp new file mode 100644 index 000000000..b5eb20ebf --- /dev/null +++ b/Source/spell_icons.cpp @@ -0,0 +1,152 @@ +#include "panels/spell_icons.hpp" + +#include "engine/load_cel.hpp" +#include "engine/render/cel_render.hpp" +#include "init.h" +#include "palette.h" +#include "utils/stdcompat/optional.hpp" + +namespace devilution { + +namespace { +std::optional pSpellCels; +uint8_t SplTransTbl[256]; +} // namespace + +const char SpellITbl[] = { + 27, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 28, + 13, + 12, + 18, + 16, + 14, + 18, + 19, + 11, + 20, + 15, + 21, + 23, + 24, + 25, + 22, + 26, + 29, + 37, + 38, + 39, + 42, + 41, + 40, + 10, + 36, + 30, + 51, + 51, + 50, + 46, + 47, + 43, + 45, + 48, + 49, + 44, + 35, + 35, + 35, + 35, + 35, +}; + +void LoadSpellIcons() +{ + if (!gbIsHellfire) + pSpellCels = LoadCel("CtrlPan\\SpelIcon.CEL", SPLICONLENGTH); + else + pSpellCels = LoadCel("Data\\SpelIcon.CEL", SPLICONLENGTH); + SetSpellTrans(RSPLTYPE_SKILL); +} + +void FreeSpellIcons() +{ + pSpellCels = std::nullopt; +} + +void DrawSpellCel(const Surface &out, Point position, int nCel) +{ + DrawSpellCel(out, position, *pSpellCels, nCel); +} + +void DrawSpellCel(const Surface &out, Point position, const CelSprite &sprite, int nCel) +{ + CelDrawLightTo(out, position, sprite, nCel, SplTransTbl); +} + +void SetSpellTrans(spell_type t) +{ + if (t == RSPLTYPE_SKILL) { + for (int i = 0; i < 128; i++) + SplTransTbl[i] = i; + } + for (int i = 128; i < 256; i++) + SplTransTbl[i] = i; + SplTransTbl[255] = 0; + + switch (t) { + case RSPLTYPE_SPELL: + SplTransTbl[PAL8_YELLOW] = PAL16_BLUE + 1; + SplTransTbl[PAL8_YELLOW + 1] = PAL16_BLUE + 3; + SplTransTbl[PAL8_YELLOW + 2] = PAL16_BLUE + 5; + for (int i = PAL16_BLUE; i < PAL16_BLUE + 16; i++) { + SplTransTbl[PAL16_BEIGE - PAL16_BLUE + i] = i; + SplTransTbl[PAL16_YELLOW - PAL16_BLUE + i] = i; + SplTransTbl[PAL16_ORANGE - PAL16_BLUE + i] = i; + } + break; + case RSPLTYPE_SCROLL: + SplTransTbl[PAL8_YELLOW] = PAL16_BEIGE + 1; + SplTransTbl[PAL8_YELLOW + 1] = PAL16_BEIGE + 3; + SplTransTbl[PAL8_YELLOW + 2] = PAL16_BEIGE + 5; + for (int i = PAL16_BEIGE; i < PAL16_BEIGE + 16; i++) { + SplTransTbl[PAL16_YELLOW - PAL16_BEIGE + i] = i; + SplTransTbl[PAL16_ORANGE - PAL16_BEIGE + i] = i; + } + break; + case RSPLTYPE_CHARGES: + SplTransTbl[PAL8_YELLOW] = PAL16_ORANGE + 1; + SplTransTbl[PAL8_YELLOW + 1] = PAL16_ORANGE + 3; + SplTransTbl[PAL8_YELLOW + 2] = PAL16_ORANGE + 5; + for (int i = PAL16_ORANGE; i < PAL16_ORANGE + 16; i++) { + SplTransTbl[PAL16_BEIGE - PAL16_ORANGE + i] = i; + SplTransTbl[PAL16_YELLOW - PAL16_ORANGE + i] = i; + } + break; + case RSPLTYPE_INVALID: + SplTransTbl[PAL8_YELLOW] = PAL16_GRAY + 1; + SplTransTbl[PAL8_YELLOW + 1] = PAL16_GRAY + 3; + SplTransTbl[PAL8_YELLOW + 2] = PAL16_GRAY + 5; + for (int i = PAL16_GRAY; i < PAL16_GRAY + 15; i++) { + SplTransTbl[PAL16_BEIGE - PAL16_GRAY + i] = i; + SplTransTbl[PAL16_YELLOW - PAL16_GRAY + i] = i; + SplTransTbl[PAL16_ORANGE - PAL16_GRAY + i] = i; + } + SplTransTbl[PAL16_BEIGE + 15] = 0; + SplTransTbl[PAL16_YELLOW + 15] = 0; + SplTransTbl[PAL16_ORANGE + 15] = 0; + break; + case RSPLTYPE_SKILL: + break; + } +} + +} // namespace devilution diff --git a/Source/spell_list.cpp b/Source/spell_list.cpp new file mode 100644 index 000000000..222776286 --- /dev/null +++ b/Source/spell_list.cpp @@ -0,0 +1,372 @@ +#include "panels/spell_list.hpp" + +#include "control.h" +#include "controls/keymapper.hpp" +#include "engine.h" +#include "engine/render/text_render.hpp" +#include "inv_iterators.hpp" +#include "palette.h" +#include "panels/spell_icons.hpp" +#include "player.h" +#include "spells.h" +#include "utils/language.h" + +#define SPLROWICONLS 10 + +namespace devilution { + +extern std::array quickSpellActionIndexes; + +namespace { + +void PrintSBookSpellType(const Surface &out, Point position, const std::string &text, uint8_t rectColorIndex) +{ + Point rect { position }; + rect += Displacement { 0, -SPLICONLENGTH + 1 }; + + // Top + DrawHorizontalLine(out, rect, SPLICONLENGTH, rectColorIndex); + DrawHorizontalLine(out, rect + Displacement { 0, 1 }, SPLICONLENGTH, rectColorIndex); + + // Bottom + DrawHorizontalLine(out, rect + Displacement { 0, SPLICONLENGTH - 2 }, SPLICONLENGTH, rectColorIndex); + DrawHorizontalLine(out, rect + Displacement { 0, SPLICONLENGTH - 1 }, SPLICONLENGTH, rectColorIndex); + + // Left Side + DrawVerticalLine(out, rect, SPLICONLENGTH, rectColorIndex); + DrawVerticalLine(out, rect + Displacement { 1, 0 }, SPLICONLENGTH, rectColorIndex); + + // Right Side + DrawVerticalLine(out, rect + Displacement { SPLICONLENGTH - 2, 0 }, SPLICONLENGTH, rectColorIndex); + DrawVerticalLine(out, rect + Displacement { SPLICONLENGTH - 1, 0 }, SPLICONLENGTH, rectColorIndex); + + // Align the spell type text with bottom of spell icon + position += Displacement { SPLICONLENGTH / 2 - GetLineWidth(text.c_str()) / 2, (IsSmallFontTall() ? -19 : -15) }; + + // Draw a drop shadow below and to the left of the text + DrawString(out, text, position + Displacement { -1, 1 }, UiFlags::ColorBlack); + DrawString(out, text, position + Displacement { -1, -1 }, UiFlags::ColorBlack); + DrawString(out, text, position + Displacement { 1, -1 }, UiFlags::ColorBlack); + // Then draw the text over the top + DrawString(out, text, position, UiFlags::ColorWhite); +} + +void PrintSBookHotkey(const Surface &out, Point position, const std::string &text) +{ + // Align the hot key text with the top-right corner of the spell icon + position += Displacement { SPLICONLENGTH - (GetLineWidth(text.c_str()) + 5), 5 - SPLICONLENGTH }; + + // Draw a drop shadow below and to the left of the text + DrawString(out, text, position + Displacement { -1, 1 }, UiFlags::ColorBlack); + // Then draw the text over the top + DrawString(out, text, position, UiFlags::ColorWhite); +} + +bool GetSpellListSelection(spell_id &pSpell, spell_type &pSplType) +{ + pSpell = spell_id::SPL_INVALID; + pSplType = spell_type::RSPLTYPE_INVALID; + auto &myPlayer = Players[MyPlayerId]; + + for (auto &spellListItem : GetSpellListItems()) { + if (spellListItem.isSelected) { + pSpell = spellListItem.id; + pSplType = spellListItem.type; + if (myPlayer._pClass == HeroClass::Monk && spellListItem.id == SPL_SEARCH) + pSplType = RSPLTYPE_SKILL; + return true; + } + } + + return false; +} + +} // namespace + +void DrawSpell(const Surface &out) +{ + auto &myPlayer = Players[MyPlayerId]; + spell_id spl = myPlayer._pRSpell; + spell_type st = myPlayer._pRSplType; + + // BUGFIX: Move the next line into the if statement to avoid OOB (SPL_INVALID is -1) (fixed) + if (st == RSPLTYPE_SPELL && spl != SPL_INVALID) { + int tlvl = myPlayer._pISplLvlAdd + myPlayer._pSplLvl[spl]; + if (CheckSpell(MyPlayerId, spl, st, true) != SpellCheckResult::Success) + st = RSPLTYPE_INVALID; + if (tlvl <= 0) + st = RSPLTYPE_INVALID; + } + if (currlevel == 0 && st != RSPLTYPE_INVALID && !spelldata[spl].sTownSpell) + st = RSPLTYPE_INVALID; + SetSpellTrans(st); + const int nCel = (spl != SPL_INVALID) ? SpellITbl[spl] : 27; + const Point position { PANEL_X + 565, PANEL_Y + 119 }; + DrawSpellCel(out, position, nCel); +} + +void DrawSpellList(const Surface &out) +{ + infostr[0] = '\0'; + ClearPanel(); + + auto &myPlayer = Players[MyPlayerId]; + + for (auto &spellListItem : GetSpellListItems()) { + const spell_id spellId = spellListItem.id; + spell_type transType = spellListItem.type; + int spellLevel = 0; + const SpellData &spellDataItem = spelldata[static_cast(spellListItem.id)]; + if (currlevel == 0 && !spellDataItem.sTownSpell) { + transType = RSPLTYPE_INVALID; + } + if (spellListItem.type == RSPLTYPE_SPELL) { + spellLevel = std::max(myPlayer._pISplLvlAdd + myPlayer._pSplLvl[spellListItem.id], 0); + if (spellLevel == 0) + transType = RSPLTYPE_INVALID; + } + + SetSpellTrans(transType); + DrawSpellCel(out, spellListItem.location, SpellITbl[static_cast(spellId)]); + + if (!spellListItem.isSelected) + continue; + + uint8_t spellColor = PAL16_GRAY + 5; + + switch (spellListItem.type) { + case RSPLTYPE_SKILL: + spellColor = PAL16_YELLOW - 46; + PrintSBookSpellType(out, spellListItem.location, _("Skill"), spellColor); + strcpy(infostr, fmt::format(_("{:s} Skill"), pgettext("spell", spellDataItem.sSkillText)).c_str()); + break; + case RSPLTYPE_SPELL: + if (myPlayer.plrlevel != 0) { + spellColor = PAL16_BLUE + 5; + } + PrintSBookSpellType(out, spellListItem.location, _("Spell"), spellColor); + strcpy(infostr, fmt::format(_("{:s} Spell"), pgettext("spell", spellDataItem.sNameText)).c_str()); + if (spellId == SPL_HBOLT) { + strcpy(tempstr, _("Damages undead only")); + AddPanelString(tempstr); + } + if (spellLevel == 0) + strcpy(tempstr, _("Spell Level 0 - Unusable")); + else + strcpy(tempstr, fmt::format(_("Spell Level {:d}"), spellLevel).c_str()); + AddPanelString(tempstr); + break; + case RSPLTYPE_SCROLL: { + if (myPlayer.plrlevel != 0) { + spellColor = PAL16_RED - 59; + } + PrintSBookSpellType(out, spellListItem.location, _("Scroll"), spellColor); + strcpy(infostr, fmt::format(_("Scroll of {:s}"), pgettext("spell", spellDataItem.sNameText)).c_str()); + const InventoryAndBeltPlayerItemsRange items { myPlayer }; + const int scrollCount = std::count_if(items.begin(), items.end(), [spellId](const Item &item) { + return item.IsScrollOf(spellId); + }); + strcpy(tempstr, fmt::format(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount), scrollCount).c_str()); + AddPanelString(tempstr); + } break; + case RSPLTYPE_CHARGES: { + if (myPlayer.plrlevel != 0) { + spellColor = PAL16_ORANGE + 5; + } + PrintSBookSpellType(out, spellListItem.location, _("Staff"), spellColor); + strcpy(infostr, fmt::format(_("Staff of {:s}"), pgettext("spell", spellDataItem.sNameText)).c_str()); + int charges = myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges; + strcpy(tempstr, fmt::format(ngettext("{:d} Charge", "{:d} Charges", charges), charges).c_str()); + AddPanelString(tempstr); + } break; + case RSPLTYPE_INVALID: + break; + } + for (int t = 0; t < 4; t++) { + if (myPlayer._pSplHotKey[t] == spellId && myPlayer._pSplTHotKey[t] == spellListItem.type) { + auto hotkeyName = keymapper.KeyNameForAction(quickSpellActionIndexes[t]); + PrintSBookHotkey(out, spellListItem.location, hotkeyName); + strcpy(tempstr, fmt::format(_("Spell Hotkey {:s}"), hotkeyName.c_str()).c_str()); + AddPanelString(tempstr); + } + } + } +} + +std::vector GetSpellListItems() +{ + std::vector spellListItems; + + uint64_t mask; + + int x = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + int y = PANEL_Y - 17; + + for (int i = RSPLTYPE_SKILL; i < RSPLTYPE_INVALID; i++) { + auto &myPlayer = Players[MyPlayerId]; + switch ((spell_type)i) { + case RSPLTYPE_SKILL: + mask = myPlayer._pAblSpells; + break; + case RSPLTYPE_SPELL: + mask = myPlayer._pMemSpells; + break; + case RSPLTYPE_SCROLL: + mask = myPlayer._pScrlSpells; + break; + case RSPLTYPE_CHARGES: + mask = myPlayer._pISpells; + break; + case RSPLTYPE_INVALID: + break; + } + int8_t j = SPL_FIREBOLT; + for (uint64_t spl = 1; j < MAX_SPELLS; spl <<= 1, j++) { + if ((mask & spl) == 0) + continue; + int lx = x; + int ly = y - SPLICONLENGTH; + bool isSelected = (MousePosition.x >= lx && MousePosition.x < lx + SPLICONLENGTH && MousePosition.y >= ly && MousePosition.y < ly + SPLICONLENGTH); + spellListItems.emplace_back(SpellListItem { { x, y }, (spell_type)i, (spell_id)j, isSelected }); + x -= SPLICONLENGTH; + if (x == PANEL_X + 12 - SPLICONLENGTH) { + x = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + y -= SPLICONLENGTH; + } + } + if (mask != 0 && x != PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS) + x -= SPLICONLENGTH; + if (x == PANEL_X + 12 - SPLICONLENGTH) { + x = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + y -= SPLICONLENGTH; + } + } + + return spellListItems; +} + +void SetSpell() +{ + spell_id pSpell; + spell_type pSplType; + + spselflag = false; + if (!GetSpellListSelection(pSpell, pSplType)) { + return; + } + + ClearPanel(); + + auto &myPlayer = Players[MyPlayerId]; + myPlayer._pRSpell = pSpell; + myPlayer._pRSplType = pSplType; + + force_redraw = 255; +} + +void SetSpeedSpell(int slot) +{ + spell_id pSpell; + spell_type pSplType; + + if (!GetSpellListSelection(pSpell, pSplType)) { + return; + } + auto &myPlayer = Players[MyPlayerId]; + for (int i = 0; i < 4; ++i) { + if (myPlayer._pSplHotKey[i] == pSpell && myPlayer._pSplTHotKey[i] == pSplType) + myPlayer._pSplHotKey[i] = SPL_INVALID; + } + myPlayer._pSplHotKey[slot] = pSpell; + myPlayer._pSplTHotKey[slot] = pSplType; +} + +void ToggleSpell(int slot) +{ + uint64_t spells; + + auto &myPlayer = Players[MyPlayerId]; + + if (myPlayer._pSplHotKey[slot] == SPL_INVALID) { + return; + } + + switch (myPlayer._pSplTHotKey[slot]) { + case RSPLTYPE_SKILL: + spells = myPlayer._pAblSpells; + break; + case RSPLTYPE_SPELL: + spells = myPlayer._pMemSpells; + break; + case RSPLTYPE_SCROLL: + spells = myPlayer._pScrlSpells; + break; + case RSPLTYPE_CHARGES: + spells = myPlayer._pISpells; + break; + case RSPLTYPE_INVALID: + return; + } + + if ((spells & GetSpellBitmask(myPlayer._pSplHotKey[slot])) != 0) { + myPlayer._pRSpell = myPlayer._pSplHotKey[slot]; + myPlayer._pRSplType = myPlayer._pSplTHotKey[slot]; + force_redraw = 255; + } +} + +void DoSpeedBook() +{ + spselflag = true; + int xo = PANEL_X + 12 + SPLICONLENGTH * 10; + int yo = PANEL_Y - 17; + int x = xo + SPLICONLENGTH / 2; + int y = yo - SPLICONLENGTH / 2; + + auto &myPlayer = Players[MyPlayerId]; + + if (myPlayer._pRSpell != SPL_INVALID) { + for (int i = RSPLTYPE_SKILL; i <= RSPLTYPE_CHARGES; i++) { + uint64_t spells; + switch (i) { + case RSPLTYPE_SKILL: + spells = myPlayer._pAblSpells; + break; + case RSPLTYPE_SPELL: + spells = myPlayer._pMemSpells; + break; + case RSPLTYPE_SCROLL: + spells = myPlayer._pScrlSpells; + break; + case RSPLTYPE_CHARGES: + spells = myPlayer._pISpells; + break; + } + uint64_t spell = 1; + for (int j = 1; j < MAX_SPELLS; j++) { + if ((spell & spells) != 0) { + if (j == myPlayer._pRSpell && i == myPlayer._pRSplType) { + x = xo + SPLICONLENGTH / 2; + y = yo - SPLICONLENGTH / 2; + } + xo -= SPLICONLENGTH; + if (xo == PANEL_X + 12 - SPLICONLENGTH) { + xo = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + yo -= SPLICONLENGTH; + } + } + spell <<= 1ULL; + } + if (spells != 0 && xo != PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS) + xo -= SPLICONLENGTH; + if (xo == PANEL_X + 12 - SPLICONLENGTH) { + xo = PANEL_X + 12 + SPLICONLENGTH * SPLROWICONLS; + yo -= SPLICONLENGTH; + } + } + } + + SetCursorPos({ x, y }); +} + +} // namespace devilution