forked from diasurgical/DevilutionX
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathspell_book.cpp
More file actions
254 lines (225 loc) · 9.96 KB
/
spell_book.cpp
File metadata and controls
254 lines (225 loc) · 9.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
#include "panels/spell_book.hpp"
#include <cstdint>
#include <optional>
#include <string>
#include <expected.hpp>
#include <fmt/format.h>
#include "control.h"
#include "engine/backbuffer_state.hpp"
#include "engine/clx_sprite.hpp"
#include "engine/load_cel.hpp"
#include "engine/load_clx.hpp"
#include "engine/rectangle.hpp"
#include "engine/render/clx_render.hpp"
#include "engine/render/text_render.hpp"
#include "game_mode.hpp"
#include "missiles.h"
#include "options.h"
#include "panels/spell_icons.hpp"
#include "panels/ui_panels.hpp"
#include "player.h"
#include "spelldat.h"
#include "utils/language.h"
#include "utils/status_macros.hpp"
namespace devilution {
namespace {
OptionalOwnedClxSpriteList spellBookButtons;
OptionalOwnedClxSpriteList spellBookBackground;
const size_t SpellBookPages = 6;
const size_t SpellBookPageEntries = 7;
constexpr uint16_t SpellBookButtonWidthDiablo = 76;
constexpr uint16_t SpellBookButtonWidthHellfire = 61;
uint16_t SpellBookButtonWidth()
{
return gbIsHellfire ? SpellBookButtonWidthHellfire : SpellBookButtonWidthDiablo;
}
/** Maps from spellbook page number and position to SpellID. */
const SpellID SpellPages[SpellBookPages][SpellBookPageEntries] = {
{ SpellID::Null, SpellID::Firebolt, SpellID::ChargedBolt, SpellID::HolyBolt, SpellID::Healing, SpellID::HealOther, SpellID::Inferno },
{ SpellID::Resurrect, SpellID::FireWall, SpellID::Telekinesis, SpellID::Lightning, SpellID::TownPortal, SpellID::Flash, SpellID::StoneCurse },
{ SpellID::Phasing, SpellID::ManaShield, SpellID::Elemental, SpellID::Fireball, SpellID::FlameWave, SpellID::ChainLightning, SpellID::Guardian },
{ SpellID::Nova, SpellID::Golem, SpellID::Teleport, SpellID::Apocalypse, SpellID::BoneSpirit, SpellID::BloodStar, SpellID::Etherealize },
{ SpellID::LightningWall, SpellID::Immolation, SpellID::Warp, SpellID::Reflect, SpellID::Berserk, SpellID::RingOfFire, SpellID::Search },
{ SpellID::Invalid, SpellID::Invalid, SpellID::Invalid, SpellID::Invalid, SpellID::Invalid, SpellID::Invalid, SpellID::Invalid }
};
SpellID GetSpellFromSpellPage(size_t page, size_t entry)
{
assert(page <= SpellBookPages && entry <= SpellBookPageEntries);
if (page == 0 && entry == 0) {
switch (InspectPlayer->_pClass) {
case HeroClass::Warrior:
return SpellID::ItemRepair;
case HeroClass::Rogue:
return SpellID::TrapDisarm;
case HeroClass::Sorcerer:
return SpellID::StaffRecharge;
case HeroClass::Monk:
return *GetOptions().Gameplay.disableSearch ? SpellID::Infravision : SpellID::Search;
case HeroClass::Bard:
return SpellID::Identify;
case HeroClass::Barbarian:
return SpellID::Rage;
}
}
return SpellPages[page][entry];
}
constexpr Size SpellBookDescription { 250, 43 };
constexpr int SpellBookDescriptionPaddingHorizontal = 2;
void PrintSBookStr(const Surface &out, Point position, std::string_view text, UiFlags flags = UiFlags::None)
{
DrawString(out, text,
Rectangle(GetPanelPosition(UiPanels::Spell, position + Displacement { SPLICONLENGTH, 0 }),
SpellBookDescription)
.inset({ SpellBookDescriptionPaddingHorizontal, 0 }),
{ .flags = UiFlags::ColorWhite | flags });
}
SpellType GetSBookTrans(SpellID ii, bool townok)
{
Player &player = *InspectPlayer;
if (player._pClass == HeroClass::Monk && ((ii == SpellID::Infravision && *GetOptions().Gameplay.disableSearch) || (ii == SpellID::Search && !*GetOptions().Gameplay.disableSearch))) {
return SpellType::Skill;
}
SpellType st = SpellType::Spell;
if ((player._pISpells & GetSpellBitmask(ii)) != 0) {
st = SpellType::Charges;
}
if ((player._pAblSpells & GetSpellBitmask(ii)) != 0) {
st = SpellType::Skill;
}
if (st == SpellType::Spell) {
if (CheckSpell(*InspectPlayer, ii, st, true) != SpellCheckResult::Success) {
st = SpellType::Invalid;
}
if (player.GetSpellLevel(ii) == 0) {
st = SpellType::Invalid;
}
}
if (townok && leveltype == DTYPE_TOWN && st != SpellType::Invalid && !GetSpellData(ii).isAllowedInTown()) {
st = SpellType::Invalid;
}
return st;
}
StringOrView GetSpellPowerText(SpellID spell, int spellLevel)
{
if (spellLevel == 0) {
return _("Unusable");
}
if (spell == SpellID::BoneSpirit) {
return _(/* TRANSLATORS: UI constraints, keep short please.*/ "Dmg: 1/3 target hp");
}
const auto [min, max] = GetDamageAmt(spell, spellLevel);
if (min == -1) {
return StringOrView {};
}
if (spell == SpellID::Healing || spell == SpellID::HealOther) {
return fmt::format(fmt::runtime(_(/* TRANSLATORS: UI constraints, keep short please.*/ "Heals: {:d} - {:d}")), min, max);
}
return fmt::format(fmt::runtime(_(/* TRANSLATORS: UI constraints, keep short please.*/ "Damage: {:d} - {:d}")), min, max);
}
} // namespace
tl::expected<void, std::string> InitSpellBook()
{
ASSIGN_OR_RETURN(spellBookBackground, LoadCelWithStatus("data\\spellbk", static_cast<uint16_t>(SidePanelSize.width)));
ASSIGN_OR_RETURN(spellBookButtons, LoadCelWithStatus("data\\spellbkb", SpellBookButtonWidth()));
return LoadSmallSpellIcons();
}
void FreeSpellBook()
{
FreeSmallSpellIcons();
spellBookButtons = std::nullopt;
spellBookBackground = std::nullopt;
}
void DrawSpellBook(const Surface &out)
{
constexpr int SpellBookButtonX = 7;
constexpr int SpellBookButtonY = 348;
ClxDraw(out, GetPanelPosition(UiPanels::Spell, { 0, 351 }), (*spellBookBackground)[0]);
const int buttonX = gbIsHellfire && SpellbookTab < 5
? SpellBookButtonWidthHellfire * SpellbookTab
: SpellBookButtonWidthDiablo * SpellbookTab
// BUGFIX: rendering of page 3 and page 4 buttons are both off-by-one pixel (fixed).
+ (SpellbookTab == 2 || SpellbookTab == 3 ? 1 : 0);
ClxDraw(out, GetPanelPosition(UiPanels::Spell, { SpellBookButtonX + buttonX, SpellBookButtonY }), (*spellBookButtons)[SpellbookTab]);
Player &player = *InspectPlayer;
uint64_t spl = player._pMemSpells | player._pISpells | player._pAblSpells;
const int lineHeight = 18;
int yp = 12;
const int textPaddingTop = 7;
for (size_t pageEntry = 0; pageEntry < SpellBookPageEntries; pageEntry++) {
SpellID sn = GetSpellFromSpellPage(SpellbookTab, pageEntry);
if (IsValidSpell(sn) && (spl & GetSpellBitmask(sn)) != 0) {
SpellType st = GetSBookTrans(sn, true);
SetSpellTrans(st);
const Point spellCellPosition = GetPanelPosition(UiPanels::Spell, { 11, yp + SpellBookDescription.height });
DrawSmallSpellIcon(out, spellCellPosition, sn);
if (sn == player._pRSpell && st == player._pRSplType && !IsInspectingPlayer()) {
SetSpellTrans(SpellType::Skill);
DrawSmallSpellIconBorder(out, spellCellPosition);
}
const Point line0 { 0, yp + textPaddingTop };
const Point line1 { 0, yp + textPaddingTop + lineHeight };
PrintSBookStr(out, line0, pgettext("spell", GetSpellData(sn).sNameText));
switch (GetSBookTrans(sn, false)) {
case SpellType::Skill:
PrintSBookStr(out, line1, _("Skill"));
break;
case SpellType::Charges: {
const int charges = player.InvBody[INVLOC_HAND_LEFT]._iCharges;
PrintSBookStr(out, line1, fmt::format(fmt::runtime(ngettext("Staff ({:d} charge)", "Staff ({:d} charges)", charges)), charges));
} break;
default: {
const int mana = GetManaAmount(player, sn) >> 6;
const int lvl = player.GetSpellLevel(sn);
PrintSBookStr(out, line0, fmt::format(fmt::runtime(pgettext(/* TRANSLATORS: UI constraints, keep short please.*/ "spellbook", "Level {:d}")), lvl), UiFlags::AlignRight);
if (const StringOrView text = GetSpellPowerText(sn, lvl); !text.empty()) {
PrintSBookStr(out, line1, text, UiFlags::AlignRight);
}
PrintSBookStr(out, line1, fmt::format(fmt::runtime(pgettext(/* TRANSLATORS: UI constraints, keep short please.*/ "spellbook", "Mana: {:d}")), mana));
} break;
}
}
yp += SpellBookDescription.height;
}
}
void CheckSBook()
{
// Icons are drawn in a column near the left side of the panel and aligned with the spell book description entries
// Spell icons/buttons are 37x38 pixels, laid out from 11,18 with a 5 pixel margin between each icon. This is close
// enough to the height of the space given to spell descriptions that we can reuse that value and subtract the
// padding from the end of the area.
Rectangle iconArea = { GetPanelPosition(UiPanels::Spell, { 11, 18 }), Size { 37, SpellBookDescription.height * 7 - 5 } };
if (iconArea.contains(MousePosition) && !IsInspectingPlayer()) {
SpellID sn = GetSpellFromSpellPage(SpellbookTab, (MousePosition.y - iconArea.position.y) / SpellBookDescription.height);
Player &player = *InspectPlayer;
uint64_t spl = player._pMemSpells | player._pISpells | player._pAblSpells;
if (IsValidSpell(sn) && (spl & GetSpellBitmask(sn)) != 0) {
SpellType st = SpellType::Spell;
if ((player._pISpells & GetSpellBitmask(sn)) != 0) {
st = SpellType::Charges;
}
if ((player._pAblSpells & GetSpellBitmask(sn)) != 0) {
st = SpellType::Skill;
}
player._pRSpell = sn;
player._pRSplType = st;
RedrawEverything();
}
return;
}
// The width of the panel excluding the border is 305 pixels. This does not cleanly divide by 4 meaning Diablo tabs
// end up with an extra pixel somewhere around the buttons. Vanilla Diablo had the buttons left-aligned, devilutionX
// instead justifies the buttons and puts the gap between buttons 2/3. See DrawSpellBook
const int buttonWidth = SpellBookButtonWidth();
// Tabs are drawn in a row near the bottom of the panel
Rectangle tabArea = { GetPanelPosition(UiPanels::Spell, { 7, 320 }), Size { 305, 29 } };
if (tabArea.contains(MousePosition)) {
int hitColumn = MousePosition.x - tabArea.position.x;
// Clicking on the gutter currently activates tab 3. Could make it do nothing by checking for == here and return early.
if (!gbIsHellfire && hitColumn > buttonWidth * 2) {
// Subtract 1 pixel to account for the gutter between buttons 2/3
hitColumn--;
}
SpellbookTab = hitColumn / buttonWidth;
}
}
} // namespace devilution