You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1907 lines
56 KiB
1907 lines
56 KiB
/** |
|
* @file control.cpp |
|
* |
|
* Implementation of the character and main control panels |
|
*/ |
|
#include "control.h" |
|
|
|
#include <algorithm> |
|
#include <array> |
|
#include <cstddef> |
|
|
|
#include <fmt/format.h> |
|
|
|
#include "DiabloUI/art.h" |
|
#include "DiabloUI/art_draw.h" |
|
#include "DiabloUI/diabloui.h" |
|
#include "automap.h" |
|
#include "controls/keymapper.hpp" |
|
#include "cursor.h" |
|
#include "engine/cel_sprite.hpp" |
|
#include "engine/load_cel.hpp" |
|
#include "engine/render/cel_render.hpp" |
|
#include "engine/render/text_render.hpp" |
|
#include "error.h" |
|
#include "gamemenu.h" |
|
#include "init.h" |
|
#include "inv.h" |
|
#include "inv_iterators.hpp" |
|
#include "lighting.h" |
|
#include "minitext.h" |
|
#include "missiles.h" |
|
#include "panels/charpanel.hpp" |
|
#include "panels/mainpanel.hpp" |
|
#include "qol/xpbar.h" |
|
#include "stores.h" |
|
#include "towners.h" |
|
#include "trigs.h" |
|
#include "utils/language.h" |
|
#include "utils/sdl_geometry.h" |
|
#include "utils/stdcompat/optional.hpp" |
|
#include "utils/utf8.h" |
|
#include "options.h" |
|
|
|
#ifdef _DEBUG |
|
#include "debug.h" |
|
#endif |
|
|
|
namespace devilution { |
|
/** |
|
* @brief Set if the life flask needs to be redrawn during next frame |
|
*/ |
|
bool drawhpflag; |
|
bool dropGoldFlag; |
|
bool chrbtn[4]; |
|
bool lvlbtndown; |
|
int dropGoldValue; |
|
/** |
|
* @brief Set if the mana flask needs to be redrawn during the next frame |
|
*/ |
|
bool drawmanaflag; |
|
bool chrbtnactive; |
|
int pnumlines; |
|
UiFlags InfoColor; |
|
char tempstr[256]; |
|
int sbooktab; |
|
int8_t initialDropGoldIndex; |
|
bool talkflag; |
|
bool sbookflag; |
|
bool chrflag; |
|
bool drawbtnflag; |
|
char infostr[128]; |
|
bool panelflag; |
|
int initialDropGoldValue; |
|
bool panbtndown; |
|
bool spselflag; |
|
Rectangle MainPanel; |
|
Rectangle LeftPanel; |
|
Rectangle RightPanel; |
|
std::optional<OwnedSurface> pBtmBuff; |
|
|
|
extern std::array<Keymapper::ActionIndex, 4> quickSpellActionIndexes; |
|
|
|
/** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */ |
|
Rectangle ChrBtnsRect[4] = { |
|
{ { 137, 138 }, { 41, 22 } }, |
|
{ { 137, 166 }, { 41, 22 } }, |
|
{ { 137, 195 }, { 41, 22 } }, |
|
{ { 137, 223 }, { 41, 22 } } |
|
}; |
|
|
|
/** Positions of panel buttons. */ |
|
SDL_Rect PanBtnPos[8] = { |
|
// clang-format off |
|
{ 9, 9, 71, 19 }, // char button |
|
{ 9, 35, 71, 19 }, // quests button |
|
{ 9, 75, 71, 19 }, // map button |
|
{ 9, 101, 71, 19 }, // menu button |
|
{ 560, 9, 71, 19 }, // inv button |
|
{ 560, 35, 71, 19 }, // spells button |
|
{ 87, 91, 33, 32 }, // chat button |
|
{ 527, 91, 33, 32 }, // friendly fire button |
|
// clang-format on |
|
}; |
|
|
|
namespace { |
|
|
|
std::optional<OwnedSurface> pLifeBuff; |
|
std::optional<OwnedSurface> pManaBuff; |
|
std::optional<CelSprite> talkButtons; |
|
std::optional<CelSprite> pDurIcons; |
|
std::optional<CelSprite> multiButtons; |
|
std::optional<CelSprite> pPanelButtons; |
|
std::optional<CelSprite> pGBoxBuff; |
|
std::optional<CelSprite> pSBkBtnCel; |
|
std::optional<CelSprite> pSBkIconCels; |
|
std::optional<CelSprite> pSpellBkCel; |
|
std::optional<CelSprite> pSpellCels; |
|
|
|
bool PanelButtons[8]; |
|
int PanelButtonIndex; |
|
char TalkSave[8][80]; |
|
uint8_t TalkSaveIndex; |
|
uint8_t NextTalkSave; |
|
char TalkMessage[MAX_SEND_STR_LEN]; |
|
bool TalkButtonsDown[3]; |
|
int sgbPlrTalkTbl; |
|
bool WhisperList[MAX_PLRS]; |
|
char panelstr[4][64]; |
|
uint8_t SplTransTbl[256]; |
|
|
|
/** 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, |
|
PanelButtonQlog, |
|
PanelButtonAutomap, |
|
PanelButtonMainmenu, |
|
PanelButtonInventory, |
|
PanelButtonSpellbook, |
|
PanelButtonSendmsg, |
|
PanelButtonFriendly, |
|
}; |
|
|
|
/** Maps from panel_button_id to hotkey name. */ |
|
const char *const PanBtnHotKey[8] = { "'c'", "'q'", N_("Tab"), N_("Esc"), "'i'", "'b'", N_("Enter"), nullptr }; |
|
/** Maps from panel_button_id to panel button description. */ |
|
const char *const PanBtnStr[8] = { |
|
N_("Character Information"), |
|
N_("Quests log"), |
|
N_("Automap"), |
|
N_("Main Menu"), |
|
N_("Inventory"), |
|
N_("Spell book"), |
|
N_("Send Message"), |
|
"" // 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 = { |
|
{ (gnScreenWidth - PANEL_WIDTH) / 2, gnScreenHeight - PANEL_HEIGHT }, |
|
{ PANEL_WIDTH, PANEL_HEIGHT } |
|
}; |
|
LeftPanel = { |
|
{ 0, 0 }, |
|
{ SPANEL_WIDTH, SPANEL_HEIGHT } |
|
}; |
|
RightPanel = { |
|
{ 0, 0 }, |
|
{ SPANEL_WIDTH, SPANEL_HEIGHT } |
|
}; |
|
|
|
#ifdef VIRTUAL_GAMEPAD |
|
LeftPanel.position.x = gnScreenWidth / 2 - LeftPanel.size.width; |
|
#else |
|
if (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width > PANEL_WIDTH) { |
|
LeftPanel.position.x = (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width - PANEL_WIDTH) / 2; |
|
} |
|
#endif |
|
LeftPanel.position.y = (gnScreenHeight - LeftPanel.size.height - PANEL_HEIGHT) / 2; |
|
|
|
#ifdef VIRTUAL_GAMEPAD |
|
RightPanel.position.x = gnScreenWidth / 2; |
|
#else |
|
RightPanel.position.x = gnScreenWidth - RightPanel.size.width - LeftPanel.position.x; |
|
#endif |
|
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, -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 |
|
* horizontal stripe of height (max-min) onto the given buffer. |
|
* @param out Target buffer. |
|
* @param position Buffer coordinate. |
|
* @param celBuf Buffer of the empty flask cel. |
|
* @param y0 Top of the flask cel section to draw. |
|
* @param y1 Bottom of the flask cel section to draw. |
|
*/ |
|
void DrawFlaskTop(const Surface &out, Point position, const Surface &celBuf, int y0, int y1) |
|
{ |
|
out.BlitFrom(celBuf, SDL_Rect { 0, static_cast<decltype(SDL_Rect {}.y)>(y0), celBuf.w(), y1 - y0 }, position); |
|
} |
|
|
|
/** |
|
* Draws the dome of the flask that protrudes above the panel top line. |
|
* It draws a rectangle of fixed width 59 and height 'h' from the source buffer |
|
* into the target buffer. |
|
* @param out The target buffer. |
|
* @param celBuf Buffer of the empty flask cel. |
|
* @param sourcePosition Source buffer start coordinate. |
|
* @param targetPosition Target buffer coordinate. |
|
* @param h How many lines of the source buffer that will be copied. |
|
*/ |
|
void DrawFlask(const Surface &out, const Surface &celBuf, Point sourcePosition, Point targetPosition, int h) |
|
{ |
|
constexpr int FlaskWidth = 59; |
|
out.BlitFromSkipColorIndexZero(celBuf, MakeSdlRect(sourcePosition.x, sourcePosition.y, FlaskWidth, h), targetPosition); |
|
} |
|
|
|
/** |
|
* @brief Draws the part of the life/mana flasks protruding above the bottom panel |
|
* @see DrawFlaskLower() |
|
* @param out The display region to draw to |
|
* @param sourceBuffer A sprite representing the appropriate background/empty flask style |
|
* @param offset X coordinate offset for where the flask should be drawn |
|
* @param fillPer How full the flask is (a value from 0 to 80) |
|
*/ |
|
void DrawFlaskUpper(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer) |
|
{ |
|
// clamping because this function only draws the top 12% of the flask display |
|
int emptyPortion = clamp(80 - fillPer, 0, 11) + 2; // +2 to account for the frame being included in the sprite |
|
|
|
// Draw the empty part of the flask |
|
DrawFlask(out, sourceBuffer, { 13, 3 }, { PANEL_LEFT + offset, PANEL_TOP - 13 }, emptyPortion); |
|
if (emptyPortion < 13) |
|
// Draw the filled part of the flask |
|
DrawFlask(out, *pBtmBuff, { offset, emptyPortion + 3 }, { PANEL_LEFT + offset, PANEL_TOP - 13 + emptyPortion }, 13 - emptyPortion); |
|
} |
|
|
|
/** |
|
* @brief Draws the part of the life/mana flasks inside the bottom panel |
|
* @see DrawFlaskUpper() |
|
* @param out The display region to draw to |
|
* @param sourceBuffer A sprite representing the appropriate background/empty flask style |
|
* @param offset X coordinate offset for where the flask should be drawn |
|
* @param fillPer How full the flask is (a value from 0 to 80) |
|
*/ |
|
void DrawFlaskLower(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer) |
|
{ |
|
int filled = clamp(fillPer, 0, 69); |
|
|
|
if (filled < 69) |
|
DrawFlaskTop(out, { PANEL_X + offset, PANEL_Y }, sourceBuffer, 16, 85 - filled); |
|
|
|
// It appears that the panel defaults to having a filled flask and DrawFlaskTop only overlays the appropriate amount of empty space. |
|
// This draw might not be necessary? |
|
if (filled > 0) |
|
DrawPanelBox(out, { offset, 85 - filled, 88, filled }, { PANEL_X + offset, PANEL_Y + 69 - filled }); |
|
} |
|
|
|
void SetButtonStateDown(int btnId) |
|
{ |
|
PanelButtons[btnId] = true; |
|
drawbtnflag = true; |
|
panbtndown = true; |
|
} |
|
|
|
void PrintInfo(const Surface &out) |
|
{ |
|
if (talkflag) |
|
return; |
|
|
|
const int LineStart[] = { 70, 58, 52, 48, 46 }; |
|
const int LineHeights[] = { 30, 24, 18, 15, 12 }; |
|
|
|
Rectangle line { { PANEL_X + 177, PANEL_Y + LineStart[pnumlines] }, { 288, 12 } }; |
|
|
|
if (infostr[0] != '\0') { |
|
DrawString(out, infostr, line, InfoColor | UiFlags::AlignCenter | UiFlags::KerningFitSpacing, 2); |
|
line.position.y += LineHeights[pnumlines]; |
|
} |
|
|
|
for (int i = 0; i < pnumlines; i++) { |
|
DrawString(out, panelstr[i], line, InfoColor | UiFlags::AlignCenter | UiFlags::KerningFitSpacing, 2); |
|
line.position.y += LineHeights[pnumlines]; |
|
} |
|
} |
|
|
|
int CapStatPointsToAdd(int remainingStatPoints, const Player &player, CharacterAttribute attribute) |
|
{ |
|
int pointsToReachCap = player.GetMaximumAttributeValue(attribute) - player.GetBaseAttributeValue(attribute); |
|
|
|
return std::min(remainingStatPoints, pointsToReachCap); |
|
} |
|
|
|
int DrawDurIcon4Item(const Surface &out, Item &pItem, int x, int c) |
|
{ |
|
if (pItem.isEmpty()) |
|
return x; |
|
if (pItem._iDurability > 5) |
|
return x; |
|
if (c == 0) { |
|
switch (pItem._itype) { |
|
case ItemType::Sword: |
|
c = 2; |
|
break; |
|
case ItemType::Axe: |
|
c = 6; |
|
break; |
|
case ItemType::Bow: |
|
c = 7; |
|
break; |
|
case ItemType::Mace: |
|
c = 5; |
|
break; |
|
case ItemType::Staff: |
|
c = 8; |
|
break; |
|
default: |
|
c = 1; |
|
break; |
|
} |
|
} |
|
if (pItem._iDurability > 2) |
|
c += 8; |
|
CelDrawTo(out, { x, -17 + PANEL_Y }, *pDurIcons, c); |
|
return x - 32 - 8; |
|
} |
|
|
|
void PrintSBookStr(const Surface &out, Point position, const char *text) |
|
{ |
|
DrawString(out, text, { GetPanelPosition(UiPanels::Spell, { SPLICONLENGTH + position.x, position.y }), { 222, 0 } }, UiFlags::ColorWhite); |
|
} |
|
|
|
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); |
|
NewCursor(player.HoldItem._iCurs + CURSOR_FIRSTITEM); |
|
} |
|
|
|
void ResetTalkMsg() |
|
{ |
|
#ifdef _DEBUG |
|
if (CheckDebugTextCommand(TalkMessage)) |
|
return; |
|
#endif |
|
|
|
uint32_t pmask = 0; |
|
|
|
for (int i = 0; i < MAX_PLRS; i++) { |
|
if (WhisperList[i]) |
|
pmask |= 1 << i; |
|
} |
|
|
|
NetSendCmdString(pmask, TalkMessage); |
|
} |
|
|
|
void ControlPressEnter() |
|
{ |
|
if (TalkMessage[0] != 0) { |
|
ResetTalkMsg(); |
|
uint8_t i = 0; |
|
for (; i < 8; i++) { |
|
if (strcmp(TalkSave[i], TalkMessage) == 0) |
|
break; |
|
} |
|
if (i >= 8) { |
|
strcpy(TalkSave[NextTalkSave], TalkMessage); |
|
NextTalkSave++; |
|
NextTalkSave &= 7; |
|
} else { |
|
uint8_t talkSave = NextTalkSave - 1; |
|
talkSave &= 7; |
|
if (i != talkSave) { |
|
strcpy(TalkSave[i], TalkSave[talkSave]); |
|
strcpy(TalkSave[talkSave], TalkMessage); |
|
} |
|
} |
|
TalkMessage[0] = '\0'; |
|
TalkSaveIndex = NextTalkSave; |
|
} |
|
control_reset_talk(); |
|
} |
|
|
|
void ControlUpDown(int v) |
|
{ |
|
for (int i = 0; i < 8; i++) { |
|
TalkSaveIndex = (v + TalkSaveIndex) & 7; |
|
if (TalkSave[TalkSaveIndex][0] != 0) { |
|
strcpy(TalkMessage, TalkSave[TalkSaveIndex]); |
|
return; |
|
} |
|
} |
|
} |
|
|
|
void RemoveGold(Player &player, int goldIndex) |
|
{ |
|
int gi = goldIndex - INVITEM_INV_FIRST; |
|
player.InvList[gi]._ivalue -= dropGoldValue; |
|
if (player.InvList[gi]._ivalue > 0) |
|
SetPlrHandGoldCurs(player.InvList[gi]); |
|
else |
|
player.RemoveInvItem(gi); |
|
SetPlrHandItem(player.HoldItem, IDI_GOLD); |
|
SetGoldSeed(player, player.HoldItem); |
|
player.HoldItem._ivalue = dropGoldValue; |
|
player.HoldItem._iStatFlag = true; |
|
ControlSetGoldCurs(player); |
|
player._pGold = CalculateGold(player); |
|
dropGoldValue = 0; |
|
} |
|
|
|
struct SpellListItem { |
|
Point location; |
|
spell_type type; |
|
spell_id id; |
|
bool isSelected; |
|
}; |
|
|
|
std::vector<SpellListItem> GetSpellListItems() |
|
{ |
|
std::vector<SpellListItem> 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; |
|
} |
|
|
|
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() |
|
{ |
|
#ifdef _DEBUG |
|
return true; |
|
#else |
|
return gbIsMultiplayer; |
|
#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<size_t>(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<size_t>(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); |
|
} |
|
} |
|
} |
|
} |
|
|
|
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(const char *str) |
|
{ |
|
strcpy(panelstr[pnumlines], str); |
|
|
|
if (pnumlines < 4) |
|
pnumlines++; |
|
} |
|
|
|
void ClearPanel() |
|
{ |
|
pnumlines = 0; |
|
} |
|
|
|
Point GetPanelPosition(UiPanels panel, Point offset) |
|
{ |
|
Displacement displacement { offset.x, offset.y }; |
|
|
|
switch (panel) { |
|
case UiPanels::Main: |
|
return MainPanel.position + displacement; |
|
case UiPanels::Quest: |
|
case UiPanels::Character: |
|
return LeftPanel.position + displacement; |
|
case UiPanels::Spell: |
|
case UiPanels::Inventory: |
|
return RightPanel.position + displacement; |
|
default: |
|
return MainPanel.position + displacement; |
|
} |
|
} |
|
|
|
void DrawPanelBox(const Surface &out, SDL_Rect srcRect, Point targetPosition) |
|
{ |
|
out.BlitFrom(*pBtmBuff, srcRect, targetPosition); |
|
} |
|
|
|
void DrawLifeFlaskUpper(const Surface &out) |
|
{ |
|
constexpr int LifeFlaskUpperOffset = 109; |
|
DrawFlaskUpper(out, *pLifeBuff, LifeFlaskUpperOffset, Players[MyPlayerId]._pHPPer); |
|
} |
|
|
|
void DrawManaFlaskUpper(const Surface &out) |
|
{ |
|
constexpr int ManaFlaskUpperOffset = 475; |
|
DrawFlaskUpper(out, *pManaBuff, ManaFlaskUpperOffset, Players[MyPlayerId]._pManaPer); |
|
} |
|
|
|
void DrawLifeFlaskLower(const Surface &out) |
|
{ |
|
constexpr int LifeFlaskLowerOffset = 96; |
|
DrawFlaskLower(out, *pLifeBuff, LifeFlaskLowerOffset, Players[MyPlayerId]._pHPPer); |
|
} |
|
|
|
void DrawManaFlaskLower(const Surface &out) |
|
{ |
|
constexpr int ManaFlaskLowerOffeset = 464; |
|
DrawFlaskLower(out, *pManaBuff, ManaFlaskLowerOffeset, Players[MyPlayerId]._pManaPer); |
|
} |
|
|
|
void control_update_life_mana() |
|
{ |
|
Players[MyPlayerId].UpdateManaPercentage(); |
|
Players[MyPlayerId].UpdateHitPointPercentage(); |
|
} |
|
|
|
void InitControlPan() |
|
{ |
|
pBtmBuff.emplace(PANEL_WIDTH, (PANEL_HEIGHT + 16) * (IsChatAvailable() ? 2 : 1)); |
|
pManaBuff.emplace(88, 88); |
|
pLifeBuff.emplace(88, 88); |
|
|
|
LoadCharPanel(); |
|
|
|
if (!gbIsHellfire) |
|
pSpellCels = LoadCel("CtrlPan\\SpelIcon.CEL", SPLICONLENGTH); |
|
else |
|
pSpellCels = LoadCel("Data\\SpelIcon.CEL", SPLICONLENGTH); |
|
SetSpellTrans(RSPLTYPE_SKILL); |
|
CelDrawUnsafeTo(*pBtmBuff, { 0, (PANEL_HEIGHT + 16) - 1 }, LoadCel("CtrlPan\\Panel8.CEL", PANEL_WIDTH), 1); |
|
{ |
|
const Point bulbsPosition { 0, 87 }; |
|
const CelSprite statusPanel = LoadCel("CtrlPan\\P8Bulbs.CEL", 88); |
|
CelDrawUnsafeTo(*pLifeBuff, bulbsPosition, statusPanel, 1); |
|
CelDrawUnsafeTo(*pManaBuff, bulbsPosition, statusPanel, 2); |
|
} |
|
talkflag = false; |
|
if (IsChatAvailable()) { |
|
CelDrawUnsafeTo(*pBtmBuff, { 0, (PANEL_HEIGHT + 16) * 2 - 1 }, LoadCel("CtrlPan\\TalkPanl.CEL", PANEL_WIDTH), 1); |
|
multiButtons = LoadCel("CtrlPan\\P8But2.CEL", 33); |
|
talkButtons = LoadCel("CtrlPan\\TalkButt.CEL", 61); |
|
sgbPlrTalkTbl = 0; |
|
TalkMessage[0] = '\0'; |
|
for (bool &whisper : WhisperList) |
|
whisper = true; |
|
for (bool &talkButtonDown : TalkButtonsDown) |
|
talkButtonDown = false; |
|
} |
|
LoadMainPanel(); |
|
panelflag = false; |
|
lvlbtndown = false; |
|
pPanelButtons = LoadCel("CtrlPan\\Panel8bu.CEL", 71); |
|
ClearPanBtn(); |
|
if (!IsChatAvailable()) |
|
PanelButtonIndex = 6; |
|
else |
|
PanelButtonIndex = 8; |
|
pChrButtons = LoadCel("Data\\CharBut.CEL", 41); |
|
for (bool &buttonEnabled : chrbtn) |
|
buttonEnabled = false; |
|
chrbtnactive = false; |
|
pDurIcons = LoadCel("Items\\DurIcons.CEL", 32); |
|
strcpy(infostr, ""); |
|
ClearPanel(); |
|
drawhpflag = true; |
|
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; |
|
} |
|
pQLogCel = LoadCel("Data\\Quest.CEL", SPANEL_WIDTH); |
|
pGBoxBuff = LoadCel("CtrlPan\\Golddrop.cel", 261); |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
initialDropGoldValue = 0; |
|
initialDropGoldIndex = 0; |
|
|
|
CalculatePanelAreas(); |
|
} |
|
|
|
void DrawCtrlPan(const Surface &out) |
|
{ |
|
DrawPanelBox(out, { 0, sgbPlrTalkTbl + 16, PANEL_WIDTH, PANEL_HEIGHT }, { PANEL_X, PANEL_Y }); |
|
DrawInfoBox(out); |
|
} |
|
|
|
void DrawCtrlBtns(const Surface &out) |
|
{ |
|
for (int i = 0; i < 6; i++) { |
|
if (!PanelButtons[i]) { |
|
DrawPanelBox(out, { PanBtnPos[i].x, PanBtnPos[i].y + 16, 71, 20 }, { PanBtnPos[i].x + PANEL_X, PanBtnPos[i].y + PANEL_Y }); |
|
} else { |
|
Point position { PanBtnPos[i].x + PANEL_X, PanBtnPos[i].y + PANEL_Y + 18 }; |
|
CelDrawTo(out, position, *pPanelButtons, i + 1); |
|
DrawArt(out, position + Displacement { 4, -18 }, &PanelButtonDown, i); |
|
} |
|
} |
|
if (PanelButtonIndex == 8) { |
|
CelDrawTo(out, { 87 + PANEL_X, 122 + PANEL_Y }, *multiButtons, PanelButtons[6] ? 2 : 1); |
|
if (gbFriendlyMode) |
|
CelDrawTo(out, { 527 + PANEL_X, 122 + PANEL_Y }, *multiButtons, PanelButtons[7] ? 4 : 3); |
|
else |
|
CelDrawTo(out, { 527 + PANEL_X, 122 + PANEL_Y }, *multiButtons, PanelButtons[7] ? 6 : 5); |
|
} |
|
} |
|
|
|
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) |
|
panelButton = false; |
|
drawbtnflag = true; |
|
panbtndown = false; |
|
} |
|
|
|
void DoPanBtn() |
|
{ |
|
for (int i = 0; i < PanelButtonIndex; i++) { |
|
int x = PanBtnPos[i].x + PANEL_LEFT + PanBtnPos[i].w; |
|
int y = PanBtnPos[i].y + PANEL_TOP + PanBtnPos[i].h; |
|
if (MousePosition.x >= PanBtnPos[i].x + PANEL_LEFT && MousePosition.x <= x) { |
|
if (MousePosition.y >= PanBtnPos[i].y + PANEL_TOP && MousePosition.y <= y) { |
|
PanelButtons[i] = true; |
|
drawbtnflag = true; |
|
panbtndown = true; |
|
} |
|
} |
|
} |
|
if (!spselflag && MousePosition.x >= 565 + PANEL_LEFT && MousePosition.x < 621 + PANEL_LEFT && MousePosition.y >= 64 + PANEL_TOP && MousePosition.y < 120 + PANEL_TOP) { |
|
if ((SDL_GetModState() & KMOD_SHIFT) != 0) { |
|
auto &myPlayer = Players[MyPlayerId]; |
|
myPlayer._pRSpell = SPL_INVALID; |
|
myPlayer._pRSplType = RSPLTYPE_INVALID; |
|
force_redraw = 255; |
|
return; |
|
} |
|
DoSpeedBook(); |
|
gamemenu_off(); |
|
} |
|
} |
|
|
|
void control_check_btn_press() |
|
{ |
|
int x = PanBtnPos[3].x + PANEL_LEFT + PanBtnPos[3].w; |
|
int y = PanBtnPos[3].y + PANEL_TOP + PanBtnPos[3].h; |
|
if (MousePosition.x >= PanBtnPos[3].x + PANEL_LEFT |
|
&& MousePosition.x <= x |
|
&& MousePosition.y >= PanBtnPos[3].y + PANEL_TOP |
|
&& MousePosition.y <= y) { |
|
SetButtonStateDown(3); |
|
} |
|
x = PanBtnPos[6].x + PANEL_LEFT + PanBtnPos[6].w; |
|
y = PanBtnPos[6].y + PANEL_TOP + PanBtnPos[6].h; |
|
if (MousePosition.x >= PanBtnPos[6].x + PANEL_LEFT |
|
&& MousePosition.x <= x |
|
&& MousePosition.y >= PanBtnPos[6].y + PANEL_TOP |
|
&& MousePosition.y <= y) { |
|
SetButtonStateDown(6); |
|
} |
|
} |
|
|
|
void DoAutoMap() |
|
{ |
|
if (currlevel != 0 || gbIsMultiplayer) { |
|
if (!AutomapActive) |
|
StartAutomap(); |
|
else |
|
AutomapActive = false; |
|
} else { |
|
InitDiabloMsg(EMSG_NO_AUTOMAP_IN_TOWN); |
|
} |
|
} |
|
|
|
void CheckPanelInfo() |
|
{ |
|
panelflag = false; |
|
ClearPanel(); |
|
for (int i = 0; i < PanelButtonIndex; i++) { |
|
int xend = PanBtnPos[i].x + PANEL_LEFT + PanBtnPos[i].w; |
|
int yend = PanBtnPos[i].y + PANEL_TOP + PanBtnPos[i].h; |
|
if (MousePosition.x >= PanBtnPos[i].x + PANEL_LEFT && MousePosition.x <= xend && MousePosition.y >= PanBtnPos[i].y + PANEL_TOP && MousePosition.y <= yend) { |
|
if (i != 7) { |
|
strcpy(infostr, _(PanBtnStr[i])); |
|
} else { |
|
if (gbFriendlyMode) |
|
strcpy(infostr, _("Player friendly")); |
|
else |
|
strcpy(infostr, _("Player attack")); |
|
} |
|
if (PanBtnHotKey[i] != nullptr) { |
|
strcpy(tempstr, fmt::format(_("Hotkey: {:s}"), _(PanBtnHotKey[i])).c_str()); |
|
AddPanelString(tempstr); |
|
} |
|
InfoColor = UiFlags::ColorWhite; |
|
panelflag = true; |
|
} |
|
} |
|
if (!spselflag && MousePosition.x >= 565 + PANEL_LEFT && MousePosition.x < 621 + PANEL_LEFT && MousePosition.y >= 64 + PANEL_TOP && MousePosition.y < 120 + PANEL_TOP) { |
|
strcpy(infostr, _("Select current spell button")); |
|
InfoColor = UiFlags::ColorWhite; |
|
panelflag = true; |
|
strcpy(tempstr, _("Hotkey: 's'")); |
|
AddPanelString(tempstr); |
|
auto &myPlayer = Players[MyPlayerId]; |
|
const spell_id spellId = myPlayer._pRSpell; |
|
if (spellId != SPL_INVALID) { |
|
switch (myPlayer._pRSplType) { |
|
case RSPLTYPE_SKILL: |
|
strcpy(tempstr, fmt::format(_("{:s} Skill"), pgettext("spell", spelldata[spellId].sSkillText)).c_str()); |
|
AddPanelString(tempstr); |
|
break; |
|
case RSPLTYPE_SPELL: { |
|
strcpy(tempstr, fmt::format(_("{:s} Spell"), pgettext("spell", spelldata[spellId].sNameText)).c_str()); |
|
AddPanelString(tempstr); |
|
int c = std::max(myPlayer._pISplLvlAdd + myPlayer._pSplLvl[spellId], 0); |
|
if (c == 0) |
|
strcpy(tempstr, _("Spell Level 0 - Unusable")); |
|
else |
|
strcpy(tempstr, fmt::format(_("Spell Level {:d}"), c).c_str()); |
|
AddPanelString(tempstr); |
|
} break; |
|
case RSPLTYPE_SCROLL: { |
|
strcpy(tempstr, fmt::format(_("Scroll of {:s}"), pgettext("spell", spelldata[spellId].sNameText)).c_str()); |
|
AddPanelString(tempstr); |
|
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: |
|
strcpy(tempstr, fmt::format(_("Staff of {:s}"), pgettext("spell", spelldata[spellId].sNameText)).c_str()); |
|
AddPanelString(tempstr); |
|
strcpy(tempstr, fmt::format(ngettext("{:d} Charge", "{:d} Charges", myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges), myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges).c_str()); |
|
AddPanelString(tempstr); |
|
break; |
|
case RSPLTYPE_INVALID: |
|
break; |
|
} |
|
} |
|
} |
|
if (MousePosition.x > 190 + PANEL_LEFT && MousePosition.x < 437 + PANEL_LEFT && MousePosition.y > 4 + PANEL_TOP && MousePosition.y < 33 + PANEL_TOP) |
|
pcursinvitem = CheckInvHLight(); |
|
|
|
if (CheckXPBarInfo()) { |
|
panelflag = true; |
|
} |
|
} |
|
|
|
void CheckBtnUp() |
|
{ |
|
bool gamemenuOff = true; |
|
drawbtnflag = true; |
|
panbtndown = false; |
|
|
|
for (int i = 0; i < 8; i++) { |
|
if (!PanelButtons[i]) { |
|
continue; |
|
} |
|
|
|
PanelButtons[i] = false; |
|
|
|
if (MousePosition.x < PanBtnPos[i].x + PANEL_LEFT |
|
|| MousePosition.x > PanBtnPos[i].x + PANEL_LEFT + PanBtnPos[i].w |
|
|| MousePosition.y < PanBtnPos[i].y + PANEL_TOP |
|
|| MousePosition.y > PanBtnPos[i].y + PANEL_TOP + PanBtnPos[i].h) { |
|
continue; |
|
} |
|
|
|
switch (i) { |
|
case PanelButtonCharinfo: |
|
QuestLogIsOpen = false; |
|
chrflag = !chrflag; |
|
break; |
|
case PanelButtonQlog: |
|
chrflag = false; |
|
if (!QuestLogIsOpen) |
|
StartQuestlog(); |
|
else |
|
QuestLogIsOpen = false; |
|
break; |
|
case PanelButtonAutomap: |
|
DoAutoMap(); |
|
break; |
|
case PanelButtonMainmenu: |
|
qtextflag = false; |
|
gamemenu_handle_previous(); |
|
gamemenuOff = false; |
|
break; |
|
case PanelButtonInventory: |
|
sbookflag = false; |
|
invflag = !invflag; |
|
if (dropGoldFlag) { |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
} |
|
break; |
|
case PanelButtonSpellbook: |
|
invflag = false; |
|
if (dropGoldFlag) { |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
} |
|
sbookflag = !sbookflag; |
|
break; |
|
case PanelButtonSendmsg: |
|
if (talkflag) |
|
control_reset_talk(); |
|
else |
|
control_type_message(); |
|
break; |
|
case PanelButtonFriendly: |
|
gbFriendlyMode = !gbFriendlyMode; |
|
break; |
|
} |
|
} |
|
|
|
if (gamemenuOff) |
|
gamemenu_off(); |
|
} |
|
|
|
void FreeControlPan() |
|
{ |
|
pBtmBuff = std::nullopt; |
|
pManaBuff = std::nullopt; |
|
pLifeBuff = std::nullopt; |
|
pSpellCels = std::nullopt; |
|
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(); |
|
} |
|
|
|
void DrawInfoBox(const Surface &out) |
|
{ |
|
DrawPanelBox(out, { 177, 62, 288, 60 }, { PANEL_X + 177, PANEL_Y + 46 }); |
|
if (!panelflag && !trigflag && pcursinvitem == -1 && !spselflag) { |
|
infostr[0] = '\0'; |
|
InfoColor = UiFlags::ColorWhite; |
|
ClearPanel(); |
|
} |
|
if (spselflag || trigflag) { |
|
InfoColor = UiFlags::ColorWhite; |
|
} else if (pcurs >= CURSOR_FIRSTITEM) { |
|
auto &myPlayer = Players[MyPlayerId]; |
|
if (myPlayer.HoldItem._itype == ItemType::Gold) { |
|
int nGold = myPlayer.HoldItem._ivalue; |
|
strcpy(infostr, fmt::format(ngettext("{:d} gold piece", "{:d} gold pieces", nGold), nGold).c_str()); |
|
} else if (!myPlayer.HoldItem._iStatFlag) { |
|
ClearPanel(); |
|
AddPanelString(_("Requirements not met")); |
|
} else { |
|
if (myPlayer.HoldItem._iIdentified) |
|
strcpy(infostr, myPlayer.HoldItem._iIName); |
|
else |
|
strcpy(infostr, myPlayer.HoldItem._iName); |
|
InfoColor = myPlayer.HoldItem.getTextColor(); |
|
} |
|
} else { |
|
if (pcursitem != -1) |
|
GetItemStr(Items[pcursitem]); |
|
else if (pcursobj != -1) |
|
GetObjectStr(pcursobj); |
|
if (pcursmonst != -1) { |
|
const auto &monster = Monsters[pcursmonst]; |
|
if (leveltype != DTYPE_TOWN) { |
|
InfoColor = UiFlags::ColorWhite; |
|
strcpy(infostr, monster.mName); |
|
ClearPanel(); |
|
if (monster._uniqtype != 0) { |
|
InfoColor = UiFlags::ColorWhitegold; |
|
PrintUniqueHistory(); |
|
} else { |
|
PrintMonstHistory(monster.MType->mtype); |
|
} |
|
} else if (pcursitem == -1) { |
|
string_view townerName = Towners[pcursmonst].name; |
|
strncpy(infostr, townerName.data(), townerName.length()); |
|
infostr[townerName.length()] = '\0'; |
|
} |
|
} |
|
if (pcursplr != -1) { |
|
InfoColor = UiFlags::ColorWhitegold; |
|
auto &target = Players[pcursplr]; |
|
strcpy(infostr, target._pName); |
|
ClearPanel(); |
|
strcpy(tempstr, fmt::format(_("{:s}, Level: {:d}"), _(ClassStrTbl[static_cast<std::size_t>(target._pClass)]), target._pLevel).c_str()); |
|
AddPanelString(tempstr); |
|
strcpy(tempstr, fmt::format(_("Hit Points {:d} of {:d}"), target._pHitPoints >> 6, target._pMaxHP >> 6).c_str()); |
|
AddPanelString(tempstr); |
|
} |
|
} |
|
if (infostr[0] != '\0' || pnumlines != 0) |
|
PrintInfo(out); |
|
} |
|
|
|
void CheckLvlBtn() |
|
{ |
|
if (!lvlbtndown && MousePosition.x >= 40 + PANEL_LEFT && MousePosition.x <= 81 + PANEL_LEFT && MousePosition.y >= -39 + PANEL_TOP && MousePosition.y <= -17 + PANEL_TOP) |
|
lvlbtndown = true; |
|
} |
|
|
|
void ReleaseLvlBtn() |
|
{ |
|
if (MousePosition.x >= 40 + PANEL_LEFT && MousePosition.x <= 81 + PANEL_LEFT && MousePosition.y >= -39 + PANEL_TOP && MousePosition.y <= -17 + PANEL_TOP) { |
|
QuestLogIsOpen = false; |
|
chrflag = true; |
|
} |
|
lvlbtndown = false; |
|
} |
|
|
|
void DrawLevelUpIcon(const Surface &out) |
|
{ |
|
if (stextflag == STORE_NONE) { |
|
int nCel = lvlbtndown ? 3 : 2; |
|
DrawString(out, _("Level Up"), { { PANEL_LEFT + 0, PANEL_TOP - 62 }, { 120, 0 } }, UiFlags::ColorWhite | UiFlags::AlignCenter); |
|
CelDrawTo(out, { 40 + PANEL_X, -17 + PANEL_Y }, *pChrButtons, nCel); |
|
} |
|
} |
|
|
|
void CheckChrBtns() |
|
{ |
|
auto &myPlayer = Players[MyPlayerId]; |
|
|
|
if (chrbtnactive || myPlayer._pStatPts == 0) |
|
return; |
|
|
|
for (auto attribute : enum_values<CharacterAttribute>()) { |
|
if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute)) |
|
continue; |
|
auto buttonId = static_cast<size_t>(attribute); |
|
Rectangle button = ChrBtnsRect[buttonId]; |
|
button.position = GetPanelPosition(UiPanels::Character, button.position); |
|
if (button.Contains(MousePosition)) { |
|
chrbtn[buttonId] = true; |
|
chrbtnactive = true; |
|
} |
|
} |
|
} |
|
|
|
void ReleaseChrBtns(bool addAllStatPoints) |
|
{ |
|
chrbtnactive = false; |
|
for (auto attribute : enum_values<CharacterAttribute>()) { |
|
auto buttonId = static_cast<size_t>(attribute); |
|
if (!chrbtn[buttonId]) |
|
continue; |
|
|
|
chrbtn[buttonId] = false; |
|
Rectangle button = ChrBtnsRect[buttonId]; |
|
button.position = GetPanelPosition(UiPanels::Character, button.position); |
|
if (button.Contains(MousePosition)) { |
|
auto &myPlayer = Players[MyPlayerId]; |
|
int statPointsToAdd = 1; |
|
if (addAllStatPoints) |
|
statPointsToAdd = CapStatPointsToAdd(myPlayer._pStatPts, myPlayer, attribute); |
|
switch (attribute) { |
|
case CharacterAttribute::Strength: |
|
NetSendCmdParam1(true, CMD_ADDSTR, statPointsToAdd); |
|
myPlayer._pStatPts -= statPointsToAdd; |
|
break; |
|
case CharacterAttribute::Magic: |
|
NetSendCmdParam1(true, CMD_ADDMAG, statPointsToAdd); |
|
myPlayer._pStatPts -= statPointsToAdd; |
|
break; |
|
case CharacterAttribute::Dexterity: |
|
NetSendCmdParam1(true, CMD_ADDDEX, statPointsToAdd); |
|
myPlayer._pStatPts -= statPointsToAdd; |
|
break; |
|
case CharacterAttribute::Vitality: |
|
NetSendCmdParam1(true, CMD_ADDVIT, statPointsToAdd); |
|
myPlayer._pStatPts -= statPointsToAdd; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
void DrawDurIcon(const Surface &out) |
|
{ |
|
bool hasRoomBetweenPanels = RightPanel.position.x - (LeftPanel.position.x + LeftPanel.size.width) >= 16 + (32 + 8 + 32 + 8 + 32 + 8 + 32) + 16; |
|
bool hasRoomUnderPanels = MainPanel.position.y - (RightPanel.position.y + RightPanel.size.height) >= 16 + 32 + 16; |
|
|
|
if (!hasRoomBetweenPanels && !hasRoomUnderPanels) { |
|
if ((chrflag || QuestLogIsOpen) && (invflag || sbookflag)) |
|
return; |
|
} |
|
|
|
int x = MainPanel.position.x + MainPanel.size.width - 32 - 16; |
|
if (!hasRoomUnderPanels) { |
|
if ((invflag || sbookflag) && MainPanel.position.x + MainPanel.size.width > RightPanel.position.x) |
|
x -= MainPanel.position.x + MainPanel.size.width - RightPanel.position.x; |
|
} |
|
|
|
auto &myPlayer = Players[MyPlayerId]; |
|
x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HEAD], x, 4); |
|
x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_CHEST], x, 3); |
|
x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_LEFT], x, 0); |
|
DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_RIGHT], x, 0); |
|
} |
|
|
|
void RedBack(const Surface &out) |
|
{ |
|
uint8_t *dst = out.begin(); |
|
uint8_t *tbl = &LightTables[4608]; |
|
for (int h = gnViewportHeight; h != 0; h--, dst += out.pitch() - gnScreenWidth) { |
|
for (int w = gnScreenWidth; w != 0; w--) { |
|
if (leveltype != DTYPE_HELL || *dst >= 32) |
|
*dst = tbl[*dst]; |
|
dst++; |
|
} |
|
} |
|
} |
|
|
|
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; |
|
|
|
int yp = 43; |
|
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 + 12 }); |
|
DrawSpellCel(out, spellCellPosition, *pSBkIconCels, SpellITbl[sn]); |
|
if (sn == myPlayer._pRSpell && st == myPlayer._pRSplType) { |
|
SetSpellTrans(RSPLTYPE_SKILL); |
|
DrawSpellCel(out, spellCellPosition, *pSBkIconCels, SPLICONLAST); |
|
} |
|
int textOffset = 7; |
|
switch (GetSBookTrans(sn, false)) { |
|
case RSPLTYPE_SKILL: |
|
strcpy(tempstr, _("Skill")); |
|
break; |
|
case RSPLTYPE_CHARGES: { |
|
int charges = myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges; |
|
strcpy(tempstr, fmt::format(ngettext("Staff ({:d} charge)", "Staff ({:d} charges)", charges), charges).c_str()); |
|
} break; |
|
default: { |
|
textOffset = 0; |
|
int mana = GetManaAmount(myPlayer, sn) >> 6; |
|
if (sn != SPL_BONESPIRIT) { |
|
int min; |
|
int max; |
|
GetDamageAmt(sn, &min, &max); |
|
if (min != -1) { |
|
strcpy(tempstr, fmt::format(_(/* TRANSLATORS: Dam refers to damage. UI constrains, keep short please.*/ "Mana: {:d} Dam: {:d} - {:d}"), mana, min, max).c_str()); |
|
} else { |
|
strcpy(tempstr, fmt::format(_(/* TRANSLATORS: Dam refers to damage. UI constrains, keep short please.*/ "Mana: {:d} Dam: n/a"), mana).c_str()); |
|
} |
|
} else { |
|
strcpy(tempstr, fmt::format(_(/* TRANSLATORS: Dam refers to damage. UI constrains, keep short please.*/ "Mana: {:d} Dam: 1/3 tgt hp"), mana).c_str()); |
|
} |
|
PrintSBookStr(out, { 10, yp }, tempstr); |
|
int lvl = std::max(myPlayer._pSplLvl[sn] + myPlayer._pISplLvlAdd, 0); |
|
if (lvl == 0) { |
|
strcpy(tempstr, _("Spell Level 0 - Unusable")); |
|
} else { |
|
strcpy(tempstr, fmt::format(_("Spell Level {:d}"), lvl).c_str()); |
|
} |
|
} break; |
|
} |
|
PrintSBookStr(out, { 10, yp + textOffset - 26 }, pgettext("spell", spelldata[sn].sNameText)); |
|
PrintSBookStr(out, { 10, yp + textOffset - 13 }, tempstr); |
|
} |
|
yp += 43; |
|
} |
|
} |
|
|
|
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 - RightPanel.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 - (RightPanel.position.x + 7)) / (gbIsHellfire ? 61 : 76); |
|
} |
|
} |
|
|
|
void DrawGoldSplit(const Surface &out, int amount) |
|
{ |
|
const int dialogX = 30; |
|
|
|
CelDrawTo(out, GetPanelPosition(UiPanels::Inventory, { dialogX, 178 }), *pGBoxBuff, 1); |
|
|
|
constexpr auto BufferSize = sizeof(tempstr) / sizeof(*tempstr); |
|
|
|
// strncpy copies up to the maximum number of characters specified, it does not ensure that a null character is |
|
// written to the end of the c-string. To be safe we specify a limit one character shorter than the buffer size and |
|
// ensure that the buffer ends in a null character manually. |
|
strncpy( |
|
tempstr, |
|
fmt::format(ngettext( |
|
/* TRANSLATORS: {:d} is a number. Dialog is shown when splitting a stash of Gold.*/ "You have {:d} gold piece. How many do you want to remove?", |
|
"You have {:d} gold pieces. How many do you want to remove?", |
|
initialDropGoldValue), |
|
initialDropGoldValue) |
|
.c_str(), |
|
BufferSize - 1); |
|
// Ensure the prompt shown to the player is terminated properly (in case the formatted/translated string ends up |
|
// being longer than 255 characters) |
|
tempstr[BufferSize - 1] = '\0'; |
|
|
|
// Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words |
|
const std::string wrapped = WordWrapString(tempstr, 200); |
|
|
|
// The split gold dialog is roughly 4 lines high, but we need at least one line for the player to input an amount. |
|
// Using a clipping region 50 units high (approx 3 lines with a lineheight of 17) to ensure there is enough room left |
|
// for the text entered by the player. |
|
DrawString(out, wrapped, { GetPanelPosition(UiPanels::Inventory, { dialogX + 31, 75 }), { 200, 50 } }, UiFlags::ColorWhitegold | UiFlags::AlignCenter, 1, 17); |
|
|
|
tempstr[0] = '\0'; |
|
if (amount > 0) { |
|
// snprintf ensures that the destination buffer ends in a null character. |
|
snprintf(tempstr, BufferSize, "%u", amount); |
|
} |
|
// Even a ten digit amount of gold only takes up about half a line. There's no need to wrap or clip text here so we |
|
// use the Point form of DrawString. |
|
DrawString(out, tempstr, GetPanelPosition(UiPanels::Inventory, { dialogX + 37, 128 }), UiFlags::ColorWhite | UiFlags::PentaCursor); |
|
} |
|
|
|
void control_drop_gold(char vkey) |
|
{ |
|
auto &myPlayer = Players[MyPlayerId]; |
|
|
|
if (myPlayer._pHitPoints >> 6 <= 0) { |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
return; |
|
} |
|
|
|
if (vkey == DVL_VK_RETURN) { |
|
if (dropGoldValue > 0) |
|
RemoveGold(myPlayer, initialDropGoldIndex); |
|
CloseGoldDrop(); |
|
} else if (vkey == DVL_VK_ESCAPE) { |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
} else if (vkey == DVL_VK_BACK) { |
|
dropGoldValue = dropGoldValue / 10; |
|
} |
|
} |
|
|
|
void DrawTalkPan(const Surface &out) |
|
{ |
|
if (!talkflag) |
|
return; |
|
|
|
force_redraw = 255; |
|
|
|
DrawPanelBox(out, { 175, sgbPlrTalkTbl + 20, 294, 5 }, { PANEL_X + 175, PANEL_Y + 4 }); |
|
int off = 0; |
|
for (int i = 293; i > 283; off++, i--) { |
|
DrawPanelBox(out, { (off / 2) + 175, sgbPlrTalkTbl + off + 25, i, 1 }, { (off / 2) + PANEL_X + 175, off + PANEL_Y + 9 }); |
|
} |
|
DrawPanelBox(out, { 185, sgbPlrTalkTbl + 35, 274, 30 }, { PANEL_X + 185, PANEL_Y + 19 }); |
|
DrawPanelBox(out, { 180, sgbPlrTalkTbl + 65, 284, 5 }, { PANEL_X + 180, PANEL_Y + 49 }); |
|
for (int i = 0; i < 10; i++) { |
|
DrawPanelBox(out, { 180, sgbPlrTalkTbl + i + 70, i + 284, 1 }, { PANEL_X + 180, i + PANEL_Y + 54 }); |
|
} |
|
DrawPanelBox(out, { 170, sgbPlrTalkTbl + 80, 310, 55 }, { PANEL_X + 170, PANEL_Y + 64 }); |
|
|
|
int x = PANEL_LEFT + 200; |
|
int y = PANEL_Y + 10; |
|
|
|
uint32_t idx = DrawString(out, TalkMessage, { { x, y }, { 250, 27 } }, UiFlags::ColorWhite | UiFlags::PentaCursor, 1, 13); |
|
if (idx < sizeof(TalkMessage)) |
|
TalkMessage[idx] = '\0'; |
|
|
|
x += 46; |
|
int talkBtn = 0; |
|
for (int i = 0; i < 4; i++) { |
|
if (i == MyPlayerId) |
|
continue; |
|
|
|
UiFlags color = UiFlags::ColorRed; |
|
const Point talkPanPosition { 172 + PANEL_X, 84 + 18 * talkBtn + PANEL_Y }; |
|
if (WhisperList[i]) { |
|
color = UiFlags::ColorWhitegold; |
|
if (TalkButtonsDown[talkBtn]) { |
|
int nCel = talkBtn != 0 ? 4 : 3; |
|
CelDrawTo(out, talkPanPosition, *talkButtons, nCel); |
|
DrawArt(out, talkPanPosition + Displacement { 4, -15 }, &TalkButton, 2); |
|
} |
|
} else { |
|
int nCel = talkBtn != 0 ? 2 : 1; |
|
if (TalkButtonsDown[talkBtn]) |
|
nCel += 4; |
|
CelDrawTo(out, talkPanPosition, *talkButtons, nCel); |
|
DrawArt(out, talkPanPosition + Displacement { 4, -15 }, &TalkButton, TalkButtonsDown[talkBtn] ? 1 : 0); |
|
} |
|
auto &player = Players[i]; |
|
if (player.plractive) { |
|
DrawString(out, player._pName, { { x, y + 60 + talkBtn * 18 }, { 204, 0 } }, color); |
|
} |
|
|
|
talkBtn++; |
|
} |
|
} |
|
|
|
bool control_check_talk_btn() |
|
{ |
|
if (!talkflag) |
|
return false; |
|
|
|
if (MousePosition.x < 172 + PANEL_LEFT) |
|
return false; |
|
if (MousePosition.y < 69 + PANEL_TOP) |
|
return false; |
|
if (MousePosition.x > 233 + PANEL_LEFT) |
|
return false; |
|
if (MousePosition.y > 123 + PANEL_TOP) |
|
return false; |
|
|
|
for (bool &talkButtonDown : TalkButtonsDown) { |
|
talkButtonDown = false; |
|
} |
|
|
|
TalkButtonsDown[(MousePosition.y - (69 + PANEL_TOP)) / 18] = true; |
|
|
|
return true; |
|
} |
|
|
|
void control_release_talk_btn() |
|
{ |
|
if (!talkflag) |
|
return; |
|
|
|
for (bool &talkButtonDown : TalkButtonsDown) |
|
talkButtonDown = false; |
|
|
|
if (MousePosition.x < 172 + PANEL_LEFT || MousePosition.y < 69 + PANEL_TOP || MousePosition.x > 233 + PANEL_LEFT || MousePosition.y > 123 + PANEL_TOP) |
|
return; |
|
|
|
int off = (MousePosition.y - (69 + PANEL_TOP)) / 18; |
|
|
|
int p = 0; |
|
for (; p < MAX_PLRS && off != -1; p++) { |
|
if (p != MyPlayerId) |
|
off--; |
|
} |
|
if (p <= MAX_PLRS) |
|
WhisperList[p - 1] = !WhisperList[p - 1]; |
|
} |
|
|
|
void control_type_message() |
|
{ |
|
if (!IsChatAvailable()) |
|
return; |
|
|
|
talkflag = true; |
|
int x = PANEL_LEFT + 200; |
|
int y = PANEL_Y + 22; |
|
SDL_Rect rect = { x, y, 250, 39 }; |
|
SDL_SetTextInputRect(&rect); |
|
TalkMessage[0] = '\0'; |
|
for (bool &talkButtonDown : TalkButtonsDown) { |
|
talkButtonDown = false; |
|
} |
|
sgbPlrTalkTbl = PANEL_HEIGHT + 16; |
|
force_redraw = 255; |
|
TalkSaveIndex = NextTalkSave; |
|
SDL_StartTextInput(); |
|
} |
|
|
|
void control_reset_talk() |
|
{ |
|
talkflag = false; |
|
SDL_StopTextInput(); |
|
sgbPlrTalkTbl = 0; |
|
force_redraw = 255; |
|
} |
|
|
|
bool IsTalkActive() |
|
{ |
|
if (!IsChatAvailable()) |
|
return false; |
|
|
|
if (!talkflag) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
void control_new_text(string_view text) |
|
{ |
|
strncat(TalkMessage, text.data(), sizeof(TalkMessage) - strlen(TalkMessage) - 1); |
|
} |
|
|
|
bool control_presskeys(int vkey) |
|
{ |
|
if (!IsChatAvailable()) |
|
return false; |
|
if (!talkflag) |
|
return false; |
|
|
|
if (vkey == DVL_VK_ESCAPE) { |
|
control_reset_talk(); |
|
} else if (vkey == DVL_VK_RETURN) { |
|
ControlPressEnter(); |
|
} else if (vkey == DVL_VK_BACK) { |
|
TalkMessage[FindLastUtf8Symbols(TalkMessage)] = '\0'; |
|
} else if (vkey == DVL_VK_DOWN) { |
|
ControlUpDown(1); |
|
} else if (vkey == DVL_VK_UP) { |
|
ControlUpDown(-1); |
|
} else if (vkey != DVL_VK_SPACE) { |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void DiabloHotkeyMsg(uint32_t dwMsg) |
|
{ |
|
if (!IsChatAvailable()) { |
|
return; |
|
} |
|
|
|
assert(dwMsg < QUICK_MESSAGE_OPTIONS); |
|
|
|
#ifdef _DEBUG |
|
if (CheckDebugTextCommand(sgOptions.Chat.szHotKeyMsgs[dwMsg])) |
|
return; |
|
#endif |
|
|
|
NetSendCmdString(0xFFFFFF, sgOptions.Chat.szHotKeyMsgs[dwMsg]); |
|
} |
|
|
|
void CloseGoldDrop() |
|
{ |
|
if (!dropGoldFlag) |
|
return; |
|
dropGoldFlag = false; |
|
SDL_StopTextInput(); |
|
} |
|
|
|
void GoldDropNewText(string_view text) |
|
{ |
|
for (char vkey : text) { |
|
int digit = vkey - '0'; |
|
if (digit >= 0 && digit <= 9) { |
|
int newGoldValue = dropGoldValue * 10; |
|
newGoldValue += digit; |
|
if (newGoldValue <= initialDropGoldValue) { |
|
dropGoldValue = newGoldValue; |
|
} |
|
} |
|
} |
|
} |
|
|
|
} // namespace devilution
|
|
|