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.
1473 lines
44 KiB
1473 lines
44 KiB
/** |
|
* @file control.cpp |
|
* |
|
* Implementation of the character and main control panels |
|
*/ |
|
#include "control.h" |
|
|
|
#include <algorithm> |
|
#include <array> |
|
#include <cstddef> |
|
#include <string> |
|
|
|
#include <fmt/format.h> |
|
|
|
#include "automap.h" |
|
#include "controls/modifier_hints.h" |
|
#include "controls/plrctrls.h" |
|
#include "cursor.h" |
|
#include "engine/backbuffer_state.hpp" |
|
#include "engine/clx_sprite.hpp" |
|
#include "engine/load_cel.hpp" |
|
#include "engine/render/clx_render.hpp" |
|
#include "engine/render/text_render.hpp" |
|
#include "engine/trn.hpp" |
|
#include "error.h" |
|
#include "gamemenu.h" |
|
#include "init.h" |
|
#include "inv.h" |
|
#include "inv_iterators.hpp" |
|
#include "levels/setmaps.h" |
|
#include "levels/trigs.h" |
|
#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 "playerdat.hpp" |
|
#include "qol/stash.h" |
|
#include "qol/xpbar.h" |
|
#include "stores.h" |
|
#include "towners.h" |
|
#include "utils/format_int.hpp" |
|
#include "utils/language.h" |
|
#include "utils/log.hpp" |
|
#include "utils/sdl_geometry.h" |
|
#include "utils/stdcompat/optional.hpp" |
|
#include "utils/str_cat.hpp" |
|
#include "utils/string_or_view.hpp" |
|
#include "utils/utf8.hpp" |
|
|
|
#ifdef _DEBUG |
|
#include "debug.h" |
|
#endif |
|
|
|
namespace devilution { |
|
|
|
bool dropGoldFlag; |
|
bool chrbtn[4]; |
|
bool lvlbtndown; |
|
int dropGoldValue; |
|
bool chrbtnactive; |
|
UiFlags InfoColor; |
|
int sbooktab; |
|
int8_t initialDropGoldIndex; |
|
bool talkflag; |
|
bool sbookflag; |
|
bool chrflag; |
|
StringOrView InfoString; |
|
bool panelflag; |
|
int initialDropGoldValue; |
|
bool panbtndown; |
|
bool spselflag; |
|
Rectangle MainPanel; |
|
Rectangle LeftPanel; |
|
Rectangle RightPanel; |
|
std::optional<OwnedSurface> pBtmBuff; |
|
OptionalOwnedClxSpriteList pGBoxBuff; |
|
|
|
const Rectangle &GetMainPanel() |
|
{ |
|
return MainPanel; |
|
} |
|
const Rectangle &GetLeftPanel() |
|
{ |
|
return LeftPanel; |
|
} |
|
const Rectangle &GetRightPanel() |
|
{ |
|
return RightPanel; |
|
} |
|
bool IsLeftPanelOpen() |
|
{ |
|
return chrflag || QuestLogIsOpen || IsStashOpen; |
|
} |
|
bool IsRightPanelOpen() |
|
{ |
|
return invflag || sbookflag; |
|
} |
|
|
|
constexpr Size IncrementAttributeButtonSize { 41, 22 }; |
|
/** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */ |
|
Rectangle ChrBtnsRect[4] = { |
|
{ { 137, 138 }, IncrementAttributeButtonSize }, |
|
{ { 137, 166 }, IncrementAttributeButtonSize }, |
|
{ { 137, 195 }, IncrementAttributeButtonSize }, |
|
{ { 137, 223 }, IncrementAttributeButtonSize } |
|
}; |
|
|
|
/** 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; |
|
OptionalOwnedClxSpriteList talkButtons; |
|
OptionalOwnedClxSpriteList pDurIcons; |
|
OptionalOwnedClxSpriteList multiButtons; |
|
OptionalOwnedClxSpriteList pPanelButtons; |
|
|
|
bool PanelButtons[8]; |
|
int PanelButtonIndex; |
|
char TalkSave[8][MAX_SEND_STR_LEN]; |
|
uint8_t TalkSaveIndex; |
|
uint8_t NextTalkSave; |
|
char TalkMessage[MAX_SEND_STR_LEN]; |
|
bool TalkButtonsDown[3]; |
|
int sgbPlrTalkTbl; |
|
bool WhisperList[MAX_PLRS]; |
|
|
|
enum panel_button_id : uint8_t { |
|
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 |
|
}; |
|
|
|
/** |
|
* 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, MakeSdlRect(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 }, GetMainPanel().position + Displacement { offset, -13 }, emptyPortion); |
|
if (emptyPortion < 13) |
|
// Draw the filled part of the flask |
|
DrawFlask(out, *pBtmBuff, { offset, emptyPortion + 3 }, GetMainPanel().position + Displacement { offset, -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, GetMainPanel().position + Displacement { offset, 0 }, 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, MakeSdlRect(offset, 85 - filled, 88, filled), GetMainPanel().position + Displacement { offset, 69 - filled }); |
|
} |
|
|
|
void SetButtonStateDown(int btnId) |
|
{ |
|
PanelButtons[btnId] = true; |
|
RedrawComponent(PanelDrawComponent::ControlButtons); |
|
panbtndown = true; |
|
} |
|
|
|
void PrintInfo(const Surface &out) |
|
{ |
|
if (talkflag) |
|
return; |
|
|
|
const int space[] = { 18, 12, 6, 3, 0 }; |
|
Rectangle infoArea { GetMainPanel().position + Displacement { 177, 46 }, { 288, 60 } }; |
|
|
|
const int newLineCount = std::count(InfoString.str().begin(), InfoString.str().end(), '\n'); |
|
const int spaceIndex = std::min(4, newLineCount); |
|
const int spacing = space[spaceIndex]; |
|
const int lineHeight = 12 + spacing; |
|
|
|
// Adjusting the line height to add spacing between lines |
|
// will also add additional space beneath the last line |
|
// which throws off the vertical centering |
|
infoArea.position.y += spacing / 2; |
|
|
|
DrawString(out, InfoString, infoArea, InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::KerningFitSpacing, 2, lineHeight); |
|
} |
|
|
|
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 = 1; |
|
break; |
|
case ItemType::Axe: |
|
c = 5; |
|
break; |
|
case ItemType::Bow: |
|
c = 6; |
|
break; |
|
case ItemType::Mace: |
|
c = 4; |
|
break; |
|
case ItemType::Staff: |
|
c = 7; |
|
break; |
|
case ItemType::Shield: |
|
default: |
|
c = 0; |
|
break; |
|
} |
|
} |
|
if (pItem._iDurability > 2) |
|
c += 8; |
|
ClxDraw(out, { x, -17 + GetMainPanel().position.y }, (*pDurIcons)[c]); |
|
return x - 32 - 8; |
|
} |
|
|
|
struct TextCmdItem { |
|
const std::string text; |
|
const std::string description; |
|
const std::string requiredParameter; |
|
std::string (*actionProc)(const string_view); |
|
}; |
|
|
|
extern std::vector<TextCmdItem> TextCmdList; |
|
|
|
std::string TextCmdHelp(const string_view parameter) |
|
{ |
|
if (parameter.empty()) { |
|
std::string ret; |
|
StrAppend(ret, _("Available Commands:")); |
|
for (const TextCmdItem &textCmd : TextCmdList) { |
|
StrAppend(ret, " ", _(textCmd.text)); |
|
} |
|
return ret; |
|
} |
|
auto textCmdIterator = std::find_if(TextCmdList.begin(), TextCmdList.end(), [&](const TextCmdItem &elem) { return elem.text == parameter; }); |
|
if (textCmdIterator == TextCmdList.end()) |
|
return StrCat(_("Command "), parameter, _(" is unkown.")); |
|
auto &textCmdItem = *textCmdIterator; |
|
if (textCmdItem.requiredParameter.empty()) |
|
return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: No additional parameter needed.")); |
|
return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: "), _(textCmdItem.requiredParameter)); |
|
} |
|
|
|
void AppendArenaOverview(std::string &ret) |
|
{ |
|
for (int arena = SL_FIRST_ARENA; arena <= SL_LAST; arena++) { |
|
StrAppend(ret, "\n", arena - SL_FIRST_ARENA + 1, " (", QuestLevelNames[arena], ")"); |
|
} |
|
} |
|
|
|
const dungeon_type DungeonTypeForArena[] = { |
|
dungeon_type::DTYPE_CATHEDRAL, // SL_ARENA_CHURCH |
|
dungeon_type::DTYPE_HELL, // SL_ARENA_HELL |
|
dungeon_type::DTYPE_HELL, // SL_ARENA_CIRCLE_OF_LIFE |
|
}; |
|
|
|
std::string TextCmdArena(const string_view parameter) |
|
{ |
|
std::string ret; |
|
if (!gbIsMultiplayer) { |
|
StrAppend(ret, _("Arenas are only supported in multiplayer.")); |
|
return ret; |
|
} |
|
|
|
if (parameter.empty()) { |
|
StrAppend(ret, _("What arena do you want to visit?")); |
|
AppendArenaOverview(ret); |
|
return ret; |
|
} |
|
|
|
int arenaNumber = atoi(parameter.data()); |
|
_setlevels arenaLevel = static_cast<_setlevels>(arenaNumber - 1 + SL_FIRST_ARENA); |
|
if (arenaNumber < 0 || !IsArenaLevel(arenaLevel)) { |
|
StrAppend(ret, _("Invalid arena-number. Valid numbers are:")); |
|
AppendArenaOverview(ret); |
|
return ret; |
|
} |
|
|
|
if (!MyPlayer->isOnLevel(0) && !MyPlayer->isOnArenaLevel()) { |
|
StrAppend(ret, _("To enter a arena, you need to be in town or another arena.")); |
|
return ret; |
|
} |
|
|
|
setlvltype = DungeonTypeForArena[arenaLevel - SL_FIRST_ARENA]; |
|
StartNewLvl(*MyPlayer, WM_DIABSETLVL, arenaLevel); |
|
return ret; |
|
} |
|
|
|
std::string TextCmdArenaPot(const string_view parameter) |
|
{ |
|
std::string ret; |
|
if (!gbIsMultiplayer) { |
|
StrAppend(ret, _("Arenas are only supported in multiplayer.")); |
|
return ret; |
|
} |
|
|
|
Player &myPlayer = *MyPlayer; |
|
|
|
for (int potNumber = std::max(1, atoi(parameter.data())); potNumber > 0; potNumber--) { |
|
Item &item = myPlayer.InvList[myPlayer._pNumInv]; |
|
InitializeItem(item, IDI_ARENAPOT); |
|
GenerateNewSeed(item); |
|
item.updateRequiredStatsCacheForPlayer(myPlayer); |
|
|
|
if (!AutoPlaceItemInBelt(myPlayer, item, true) && !AutoPlaceItemInInventory(myPlayer, item, true)) { |
|
break; // inventory is full |
|
} |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
std::vector<TextCmdItem> TextCmdList = { |
|
{ N_("/help"), N_("Prints help overview or help for a specific command."), N_("({command})"), &TextCmdHelp }, |
|
{ N_("/arena"), N_("Enter a PvP Arena."), N_("{arena-number}"), &TextCmdArena }, |
|
{ N_("/arenapot"), N_("Gives Arena Potions."), N_("{number}"), &TextCmdArenaPot }, |
|
}; |
|
|
|
bool CheckTextCommand(const string_view text) |
|
{ |
|
if (text.size() < 1 || text[0] != '/') |
|
return false; |
|
|
|
auto textCmdIterator = std::find_if(TextCmdList.begin(), TextCmdList.end(), [&](const TextCmdItem &elem) { return text.find(elem.text) == 0 && (text.length() == elem.text.length() || text[elem.text.length()] == ' '); }); |
|
if (textCmdIterator == TextCmdList.end()) { |
|
InitDiabloMsg(StrCat(_("Command \""), text, "\" is unknown.")); |
|
return true; |
|
} |
|
|
|
TextCmdItem &textCmd = *textCmdIterator; |
|
string_view parameter = ""; |
|
if (text.length() > (textCmd.text.length() + 1)) |
|
parameter = text.substr(textCmd.text.length() + 1); |
|
const std::string result = textCmd.actionProc(parameter); |
|
if (result != "") |
|
InitDiabloMsg(result); |
|
return true; |
|
} |
|
|
|
void ResetTalkMsg() |
|
{ |
|
#ifdef _DEBUG |
|
if (CheckDebugTextCommand(TalkMessage)) |
|
return; |
|
#endif |
|
if (CheckTextCommand(TalkMessage)) |
|
return; |
|
|
|
uint32_t pmask = 0; |
|
|
|
for (size_t i = 0; i < Players.size(); 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]); |
|
NetSyncInvItem(player, gi); |
|
} else { |
|
player.RemoveInvItem(gi); |
|
} |
|
|
|
MakeGoldStack(player.HoldItem, dropGoldValue); |
|
NewCursor(player.HoldItem); |
|
|
|
player._pGold = CalculateGold(player); |
|
dropGoldValue = 0; |
|
} |
|
|
|
bool IsLevelUpButtonVisible() |
|
{ |
|
if (spselflag || chrflag || MyPlayer->_pStatPts == 0) { |
|
return false; |
|
} |
|
if (ControlMode == ControlTypes::VirtualGamepad) { |
|
return false; |
|
} |
|
if (stextflag != TalkID::None || IsStashOpen) { |
|
return false; |
|
} |
|
if (QuestLogIsOpen && GetLeftPanel().contains(GetMainPanel().position + Displacement { 0, -74 })) { |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
} // namespace |
|
|
|
void CalculatePanelAreas() |
|
{ |
|
constexpr Size MainPanelSize { 640, 128 }; |
|
|
|
MainPanel = { |
|
{ (gnScreenWidth - MainPanelSize.width) / 2, gnScreenHeight - MainPanelSize.height }, |
|
MainPanelSize |
|
}; |
|
LeftPanel = { |
|
{ 0, 0 }, |
|
SidePanelSize |
|
}; |
|
RightPanel = { |
|
{ 0, 0 }, |
|
SidePanelSize |
|
}; |
|
|
|
if (ControlMode == ControlTypes::VirtualGamepad) { |
|
LeftPanel.position.x = gnScreenWidth / 2 - LeftPanel.size.width; |
|
} else { |
|
if (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width > MainPanel.size.width) { |
|
LeftPanel.position.x = (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width - MainPanel.size.width) / 2; |
|
} |
|
} |
|
LeftPanel.position.y = (gnScreenHeight - LeftPanel.size.height - MainPanel.size.height) / 2; |
|
|
|
if (ControlMode == ControlTypes::VirtualGamepad) { |
|
RightPanel.position.x = gnScreenWidth / 2; |
|
} else { |
|
RightPanel.position.x = gnScreenWidth - RightPanel.size.width - LeftPanel.position.x; |
|
} |
|
RightPanel.position.y = LeftPanel.position.y; |
|
|
|
gnViewportHeight = gnScreenHeight; |
|
if (gnScreenWidth <= MainPanel.size.width) { |
|
// Part of the screen is fully obscured by the UI |
|
gnViewportHeight -= MainPanel.size.height; |
|
} |
|
} |
|
|
|
bool IsChatAvailable() |
|
{ |
|
#ifdef _DEBUG |
|
return true; |
|
#else |
|
return gbIsMultiplayer; |
|
#endif |
|
} |
|
|
|
void FocusOnCharInfo() |
|
{ |
|
Player &myPlayer = *MyPlayer; |
|
|
|
if (invflag || myPlayer._pStatPts <= 0) |
|
return; |
|
|
|
// Find the first incrementable stat. |
|
int stat = -1; |
|
for (auto attribute : enum_values<CharacterAttribute>()) { |
|
if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute)) |
|
continue; |
|
stat = static_cast<int>(attribute); |
|
} |
|
if (stat == -1) |
|
return; |
|
|
|
SetCursorPos(ChrBtnsRect[stat].Center()); |
|
} |
|
|
|
void AddPanelString(string_view str) |
|
{ |
|
if (InfoString.empty()) |
|
InfoString = str; |
|
else |
|
InfoString = StrCat(InfoString, "\n", str); |
|
} |
|
|
|
void AddPanelString(std::string &&str) |
|
{ |
|
if (InfoString.empty()) |
|
InfoString = std::move(str); |
|
else |
|
InfoString = StrCat(InfoString, "\n", str); |
|
} |
|
|
|
Point GetPanelPosition(UiPanels panel, Point offset) |
|
{ |
|
Displacement displacement { offset.x, offset.y }; |
|
|
|
switch (panel) { |
|
case UiPanels::Main: |
|
return GetMainPanel().position + displacement; |
|
case UiPanels::Quest: |
|
case UiPanels::Character: |
|
case UiPanels::Stash: |
|
return GetLeftPanel().position + displacement; |
|
case UiPanels::Spell: |
|
case UiPanels::Inventory: |
|
return GetRightPanel().position + displacement; |
|
default: |
|
return GetMainPanel().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, MyPlayer->_pHPPer); |
|
} |
|
|
|
void DrawManaFlaskUpper(const Surface &out) |
|
{ |
|
constexpr int ManaFlaskUpperOffset = 475; |
|
DrawFlaskUpper(out, *pManaBuff, ManaFlaskUpperOffset, MyPlayer->_pManaPer); |
|
} |
|
|
|
void DrawLifeFlaskLower(const Surface &out) |
|
{ |
|
constexpr int LifeFlaskLowerOffset = 96; |
|
DrawFlaskLower(out, *pLifeBuff, LifeFlaskLowerOffset, MyPlayer->_pHPPer); |
|
} |
|
|
|
void DrawManaFlaskLower(const Surface &out) |
|
{ |
|
constexpr int ManaFlaskLowerOffeset = 464; |
|
DrawFlaskLower(out, *pManaBuff, ManaFlaskLowerOffeset, MyPlayer->_pManaPer); |
|
} |
|
|
|
void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue) |
|
{ |
|
UiFlags color = (currValue > 0 ? (currValue == maxValue ? UiFlags::ColorGold : UiFlags::ColorWhite) : UiFlags::ColorRed); |
|
|
|
auto drawStringWithShadow = [out, color](string_view text, Point pos) { |
|
DrawString(out, text, pos + Displacement { -1, -1 }, UiFlags::ColorBlack | UiFlags::KerningFitSpacing, 0); |
|
DrawString(out, text, pos, color | UiFlags::KerningFitSpacing, 0); |
|
}; |
|
|
|
std::string currText = StrCat(currValue); |
|
drawStringWithShadow(currText, pos - Displacement { GetLineWidth(currText, GameFont12) + 1, 0 }); |
|
drawStringWithShadow("/", pos); |
|
drawStringWithShadow(StrCat(maxValue), pos + Displacement { GetLineWidth("/", GameFont12) + 1, 0 }); |
|
} |
|
|
|
void control_update_life_mana() |
|
{ |
|
MyPlayer->UpdateManaPercentage(); |
|
MyPlayer->UpdateHitPointPercentage(); |
|
} |
|
|
|
void InitControlPan() |
|
{ |
|
if (!HeadlessMode) { |
|
pBtmBuff.emplace(GetMainPanel().size.width, (GetMainPanel().size.height + 16) * (IsChatAvailable() ? 2 : 1)); |
|
pManaBuff.emplace(88, 88); |
|
pLifeBuff.emplace(88, 88); |
|
|
|
LoadCharPanel(); |
|
LoadLargeSpellIcons(); |
|
{ |
|
const OwnedClxSpriteList sprite = LoadCel("ctrlpan\\panel8", GetMainPanel().size.width); |
|
ClxDraw(*pBtmBuff, { 0, (GetMainPanel().size.height + 16) - 1 }, sprite[0]); |
|
} |
|
{ |
|
const Point bulbsPosition { 0, 87 }; |
|
const OwnedClxSpriteList statusPanel = LoadCel("ctrlpan\\p8bulbs", 88); |
|
ClxDraw(*pLifeBuff, bulbsPosition, statusPanel[0]); |
|
ClxDraw(*pManaBuff, bulbsPosition, statusPanel[1]); |
|
} |
|
} |
|
talkflag = false; |
|
if (IsChatAvailable()) { |
|
if (!HeadlessMode) { |
|
{ |
|
const OwnedClxSpriteList sprite = LoadCel("ctrlpan\\talkpanl", GetMainPanel().size.width); |
|
ClxDraw(*pBtmBuff, { 0, (GetMainPanel().size.height + 16) * 2 - 1 }, sprite[0]); |
|
} |
|
multiButtons = LoadCel("ctrlpan\\p8but2", 33); |
|
talkButtons = LoadCel("ctrlpan\\talkbutt", 61); |
|
} |
|
sgbPlrTalkTbl = 0; |
|
TalkMessage[0] = '\0'; |
|
for (bool &whisper : WhisperList) |
|
whisper = true; |
|
for (bool &talkButtonDown : TalkButtonsDown) |
|
talkButtonDown = false; |
|
} |
|
panelflag = false; |
|
lvlbtndown = false; |
|
if (!HeadlessMode) { |
|
LoadMainPanel(); |
|
pPanelButtons = LoadCel("ctrlpan\\panel8bu", 71); |
|
|
|
static const uint16_t CharButtonsFrameWidths[9] { 95, 41, 41, 41, 41, 41, 41, 41, 41 }; |
|
pChrButtons = LoadCel("data\\charbut", CharButtonsFrameWidths); |
|
} |
|
ClearPanBtn(); |
|
if (!IsChatAvailable()) |
|
PanelButtonIndex = 6; |
|
else |
|
PanelButtonIndex = 8; |
|
if (!HeadlessMode) |
|
pDurIcons = LoadCel("items\\duricons", 32); |
|
for (bool &buttonEnabled : chrbtn) |
|
buttonEnabled = false; |
|
chrbtnactive = false; |
|
InfoString = {}; |
|
RedrawComponent(PanelDrawComponent::Health); |
|
RedrawComponent(PanelDrawComponent::Mana); |
|
chrflag = false; |
|
spselflag = false; |
|
sbooktab = 0; |
|
sbookflag = false; |
|
|
|
if (!HeadlessMode) { |
|
InitSpellBook(); |
|
pQLogCel = LoadCel("data\\quest", static_cast<uint16_t>(SidePanelSize.width)); |
|
pGBoxBuff = LoadCel("ctrlpan\\golddrop", 261); |
|
} |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
initialDropGoldValue = 0; |
|
initialDropGoldIndex = 0; |
|
|
|
CalculatePanelAreas(); |
|
|
|
if (!HeadlessMode) |
|
InitModifierHints(); |
|
} |
|
|
|
void DrawCtrlPan(const Surface &out) |
|
{ |
|
DrawPanelBox(out, MakeSdlRect(0, sgbPlrTalkTbl + 16, GetMainPanel().size.width, GetMainPanel().size.height), GetMainPanel().position); |
|
DrawInfoBox(out); |
|
} |
|
|
|
void DrawCtrlBtns(const Surface &out) |
|
{ |
|
const Point mainPanelPosition = GetMainPanel().position; |
|
for (int i = 0; i < 6; i++) { |
|
if (!PanelButtons[i]) { |
|
DrawPanelBox(out, MakeSdlRect(PanBtnPos[i].x, PanBtnPos[i].y + 16, 71, 20), mainPanelPosition + Displacement { PanBtnPos[i].x, PanBtnPos[i].y }); |
|
} else { |
|
Point position = mainPanelPosition + Displacement { PanBtnPos[i].x, PanBtnPos[i].y + 18 }; |
|
ClxDraw(out, position, (*pPanelButtons)[i]); |
|
RenderClxSprite(out, (*PanelButtonDown)[i], position + Displacement { 4, -18 }); |
|
} |
|
} |
|
|
|
if (PanelButtonIndex == 8) { |
|
ClxDraw(out, mainPanelPosition + Displacement { 87, 122 }, (*multiButtons)[PanelButtons[6] ? 1 : 0]); |
|
if (MyPlayer->friendlyMode) |
|
ClxDraw(out, mainPanelPosition + Displacement { 527, 122 }, (*multiButtons)[PanelButtons[7] ? 3 : 2]); |
|
else |
|
ClxDraw(out, mainPanelPosition + Displacement { 527, 122 }, (*multiButtons)[PanelButtons[7] ? 5 : 4]); |
|
} |
|
} |
|
|
|
void ClearPanBtn() |
|
{ |
|
for (bool &panelButton : PanelButtons) |
|
panelButton = false; |
|
RedrawComponent(PanelDrawComponent::ControlButtons); |
|
panbtndown = false; |
|
} |
|
|
|
void DoPanBtn() |
|
{ |
|
const Point mainPanelPosition = GetMainPanel().position; |
|
|
|
for (int i = 0; i < PanelButtonIndex; i++) { |
|
int x = PanBtnPos[i].x + mainPanelPosition.x + PanBtnPos[i].w; |
|
int y = PanBtnPos[i].y + mainPanelPosition.y + PanBtnPos[i].h; |
|
if (MousePosition.x >= PanBtnPos[i].x + mainPanelPosition.x && MousePosition.x <= x) { |
|
if (MousePosition.y >= PanBtnPos[i].y + mainPanelPosition.y && MousePosition.y <= y) { |
|
PanelButtons[i] = true; |
|
RedrawComponent(PanelDrawComponent::ControlButtons); |
|
panbtndown = true; |
|
} |
|
} |
|
} |
|
if (!spselflag && MousePosition.x >= 565 + mainPanelPosition.x && MousePosition.x < 621 + mainPanelPosition.x && MousePosition.y >= 64 + mainPanelPosition.y && MousePosition.y < 120 + mainPanelPosition.y) { |
|
if ((SDL_GetModState() & KMOD_SHIFT) != 0) { |
|
Player &myPlayer = *MyPlayer; |
|
myPlayer._pRSpell = SpellID::Invalid; |
|
myPlayer._pRSplType = SpellType::Invalid; |
|
RedrawEverything(); |
|
return; |
|
} |
|
DoSpeedBook(); |
|
gamemenu_off(); |
|
} |
|
} |
|
|
|
void control_check_btn_press() |
|
{ |
|
const Point mainPanelPosition = GetMainPanel().position; |
|
int x = PanBtnPos[3].x + mainPanelPosition.x + PanBtnPos[3].w; |
|
int y = PanBtnPos[3].y + mainPanelPosition.y + PanBtnPos[3].h; |
|
if (MousePosition.x >= PanBtnPos[3].x + mainPanelPosition.x |
|
&& MousePosition.x <= x |
|
&& MousePosition.y >= PanBtnPos[3].y + mainPanelPosition.y |
|
&& MousePosition.y <= y) { |
|
SetButtonStateDown(3); |
|
} |
|
x = PanBtnPos[6].x + mainPanelPosition.x + PanBtnPos[6].w; |
|
y = PanBtnPos[6].y + mainPanelPosition.y + PanBtnPos[6].h; |
|
if (MousePosition.x >= PanBtnPos[6].x + mainPanelPosition.x |
|
&& MousePosition.x <= x |
|
&& MousePosition.y >= PanBtnPos[6].y + mainPanelPosition.y |
|
&& MousePosition.y <= y) { |
|
SetButtonStateDown(6); |
|
} |
|
} |
|
|
|
void DoAutoMap() |
|
{ |
|
if (!AutomapActive) |
|
StartAutomap(); |
|
else |
|
AutomapActive = false; |
|
} |
|
|
|
void CheckPanelInfo() |
|
{ |
|
panelflag = false; |
|
const Point mainPanelPosition = GetMainPanel().position; |
|
for (int i = 0; i < PanelButtonIndex; i++) { |
|
int xend = PanBtnPos[i].x + mainPanelPosition.x + PanBtnPos[i].w; |
|
int yend = PanBtnPos[i].y + mainPanelPosition.y + PanBtnPos[i].h; |
|
if (MousePosition.x >= PanBtnPos[i].x + mainPanelPosition.x && MousePosition.x <= xend && MousePosition.y >= PanBtnPos[i].y + mainPanelPosition.y && MousePosition.y <= yend) { |
|
if (i != 7) { |
|
InfoString = _(PanBtnStr[i]); |
|
} else { |
|
if (MyPlayer->friendlyMode) |
|
InfoString = _("Player friendly"); |
|
else |
|
InfoString = _("Player attack"); |
|
} |
|
if (PanBtnHotKey[i] != nullptr) { |
|
AddPanelString(fmt::format(fmt::runtime(_("Hotkey: {:s}")), _(PanBtnHotKey[i]))); |
|
} |
|
InfoColor = UiFlags::ColorWhite; |
|
panelflag = true; |
|
} |
|
} |
|
if (!spselflag && MousePosition.x >= 565 + mainPanelPosition.x && MousePosition.x < 621 + mainPanelPosition.x && MousePosition.y >= 64 + mainPanelPosition.y && MousePosition.y < 120 + mainPanelPosition.y) { |
|
InfoString = _("Select current spell button"); |
|
InfoColor = UiFlags::ColorWhite; |
|
panelflag = true; |
|
AddPanelString(_("Hotkey: 's'")); |
|
Player &myPlayer = *MyPlayer; |
|
const SpellID spellId = myPlayer._pRSpell; |
|
if (IsValidSpell(spellId)) { |
|
switch (myPlayer._pRSplType) { |
|
case SpellType::Skill: |
|
AddPanelString(fmt::format(fmt::runtime(_("{:s} Skill")), pgettext("spell", GetSpellData(spellId).sNameText))); |
|
break; |
|
case SpellType::Spell: { |
|
AddPanelString(fmt::format(fmt::runtime(_("{:s} Spell")), pgettext("spell", GetSpellData(spellId).sNameText))); |
|
const int spellLevel = myPlayer.GetSpellLevel(spellId); |
|
AddPanelString(spellLevel == 0 ? _("Spell Level 0 - Unusable") : fmt::format(fmt::runtime(_("Spell Level {:d}")), spellLevel)); |
|
} break; |
|
case SpellType::Scroll: { |
|
AddPanelString(fmt::format(fmt::runtime(_("Scroll of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); |
|
const InventoryAndBeltPlayerItemsRange items { myPlayer }; |
|
const int scrollCount = std::count_if(items.begin(), items.end(), [spellId](const Item &item) { |
|
return item.isScrollOf(spellId); |
|
}); |
|
AddPanelString(fmt::format(fmt::runtime(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount)), scrollCount)); |
|
} break; |
|
case SpellType::Charges: |
|
AddPanelString(fmt::format(fmt::runtime(_("Staff of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); |
|
AddPanelString(fmt::format(fmt::runtime(ngettext("{:d} Charge", "{:d} Charges", myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges)), myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges)); |
|
break; |
|
case SpellType::Invalid: |
|
break; |
|
} |
|
} |
|
} |
|
if (MousePosition.x > 190 + mainPanelPosition.x && MousePosition.x < 437 + mainPanelPosition.x && MousePosition.y > 4 + mainPanelPosition.y && MousePosition.y < 33 + mainPanelPosition.y) |
|
pcursinvitem = CheckInvHLight(); |
|
|
|
if (CheckXPBarInfo()) { |
|
panelflag = true; |
|
} |
|
} |
|
|
|
void CheckBtnUp() |
|
{ |
|
bool gamemenuOff = true; |
|
const Point mainPanelPosition = GetMainPanel().position; |
|
|
|
RedrawComponent(PanelDrawComponent::ControlButtons); |
|
panbtndown = false; |
|
|
|
for (int i = 0; i < 8; i++) { |
|
if (!PanelButtons[i]) { |
|
continue; |
|
} |
|
|
|
PanelButtons[i] = false; |
|
|
|
if (MousePosition.x < PanBtnPos[i].x + mainPanelPosition.x |
|
|| MousePosition.x > PanBtnPos[i].x + mainPanelPosition.x + PanBtnPos[i].w |
|
|| MousePosition.y < PanBtnPos[i].y + mainPanelPosition.y |
|
|| MousePosition.y > PanBtnPos[i].y + mainPanelPosition.y + PanBtnPos[i].h) { |
|
continue; |
|
} |
|
|
|
switch (i) { |
|
case PanelButtonCharinfo: |
|
QuestLogIsOpen = false; |
|
CloseGoldWithdraw(); |
|
IsStashOpen = false; |
|
chrflag = !chrflag; |
|
break; |
|
case PanelButtonQlog: |
|
chrflag = false; |
|
CloseGoldWithdraw(); |
|
IsStashOpen = 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; |
|
CloseGoldWithdraw(); |
|
IsStashOpen = false; |
|
invflag = !invflag; |
|
if (dropGoldFlag) { |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
} |
|
break; |
|
case PanelButtonSpellbook: |
|
CloseInventory(); |
|
if (dropGoldFlag) { |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
} |
|
sbookflag = !sbookflag; |
|
break; |
|
case PanelButtonSendmsg: |
|
if (talkflag) |
|
control_reset_talk(); |
|
else |
|
control_type_message(); |
|
break; |
|
case PanelButtonFriendly: |
|
// Toggle friendly Mode |
|
NetSendCmd(true, CMD_FRIENDLYMODE); |
|
break; |
|
} |
|
} |
|
|
|
if (gamemenuOff) |
|
gamemenu_off(); |
|
} |
|
|
|
void FreeControlPan() |
|
{ |
|
pBtmBuff = std::nullopt; |
|
pManaBuff = std::nullopt; |
|
pLifeBuff = std::nullopt; |
|
FreeLargeSpellIcons(); |
|
FreeSpellBook(); |
|
pPanelButtons = std::nullopt; |
|
multiButtons = std::nullopt; |
|
talkButtons = std::nullopt; |
|
pChrButtons = std::nullopt; |
|
pDurIcons = std::nullopt; |
|
pQLogCel = std::nullopt; |
|
pGBoxBuff = std::nullopt; |
|
FreeMainPanel(); |
|
FreeCharPanel(); |
|
FreeModifierHints(); |
|
} |
|
|
|
void DrawInfoBox(const Surface &out) |
|
{ |
|
DrawPanelBox(out, { 177, 62, 288, 63 }, GetMainPanel().position + Displacement { 177, 46 }); |
|
if (!panelflag && !trigflag && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && !spselflag) { |
|
InfoString = {}; |
|
InfoColor = UiFlags::ColorWhite; |
|
} |
|
Player &myPlayer = *MyPlayer; |
|
if (spselflag || trigflag) { |
|
InfoColor = UiFlags::ColorWhite; |
|
} else if (!myPlayer.HoldItem.isEmpty()) { |
|
if (myPlayer.HoldItem._itype == ItemType::Gold) { |
|
int nGold = myPlayer.HoldItem._ivalue; |
|
InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); |
|
} else if (!myPlayer.CanUseItem(myPlayer.HoldItem)) { |
|
InfoString = _("Requirements not met"); |
|
} else { |
|
InfoString = myPlayer.HoldItem.getName(); |
|
InfoColor = myPlayer.HoldItem.getTextColor(); |
|
} |
|
} else { |
|
if (pcursitem != -1) |
|
GetItemStr(Items[pcursitem]); |
|
else if (ObjectUnderCursor != nullptr) |
|
GetObjectStr(*ObjectUnderCursor); |
|
if (pcursmonst != -1) { |
|
if (leveltype != DTYPE_TOWN) { |
|
const auto &monster = Monsters[pcursmonst]; |
|
InfoColor = UiFlags::ColorWhite; |
|
InfoString = monster.name(); |
|
if (monster.isUnique()) { |
|
InfoColor = UiFlags::ColorWhitegold; |
|
PrintUniqueHistory(); |
|
} else { |
|
PrintMonstHistory(monster.type().type); |
|
} |
|
} else if (pcursitem == -1) { |
|
InfoString = string_view(Towners[pcursmonst].name); |
|
} |
|
} |
|
if (pcursplr != -1) { |
|
InfoColor = UiFlags::ColorWhitegold; |
|
auto &target = Players[pcursplr]; |
|
InfoString = string_view(target._pName); |
|
AddPanelString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), _(PlayersData[static_cast<std::size_t>(target._pClass)].className), target._pLevel)); |
|
AddPanelString(fmt::format(fmt::runtime(_("Hit Points {:d} of {:d}")), target._pHitPoints >> 6, target._pMaxHP >> 6)); |
|
} |
|
} |
|
if (!InfoString.empty()) |
|
PrintInfo(out); |
|
} |
|
|
|
void CheckLvlBtn() |
|
{ |
|
if (!IsLevelUpButtonVisible()) { |
|
return; |
|
} |
|
|
|
const Point mainPanelPosition = GetMainPanel().position; |
|
if (!lvlbtndown && MousePosition.x >= 40 + mainPanelPosition.x && MousePosition.x <= 81 + mainPanelPosition.x && MousePosition.y >= -39 + mainPanelPosition.y && MousePosition.y <= -17 + mainPanelPosition.y) |
|
lvlbtndown = true; |
|
} |
|
|
|
void ReleaseLvlBtn() |
|
{ |
|
const Point mainPanelPosition = GetMainPanel().position; |
|
if (MousePosition.x >= 40 + mainPanelPosition.x && MousePosition.x <= 81 + mainPanelPosition.x && MousePosition.y >= -39 + mainPanelPosition.y && MousePosition.y <= -17 + mainPanelPosition.y) { |
|
QuestLogIsOpen = false; |
|
CloseGoldWithdraw(); |
|
IsStashOpen = false; |
|
chrflag = true; |
|
} |
|
lvlbtndown = false; |
|
} |
|
|
|
void DrawLevelUpIcon(const Surface &out) |
|
{ |
|
if (IsLevelUpButtonVisible()) { |
|
int nCel = lvlbtndown ? 2 : 1; |
|
DrawString(out, _("Level Up"), { GetMainPanel().position + Displacement { 0, -62 }, { 120, 0 } }, UiFlags::ColorWhite | UiFlags::AlignCenter); |
|
ClxDraw(out, GetMainPanel().position + Displacement { 40, -17 }, (*pChrButtons)[nCel]); |
|
} |
|
} |
|
|
|
void CheckChrBtns() |
|
{ |
|
Player &myPlayer = *MyPlayer; |
|
|
|
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)) { |
|
Player &myPlayer = *MyPlayer; |
|
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 (IsLeftPanelOpen() && IsRightPanelOpen()) |
|
return; |
|
} |
|
|
|
int x = MainPanel.position.x + MainPanel.size.width - 32 - 16; |
|
if (!hasRoomUnderPanels) { |
|
if (IsRightPanelOpen() && MainPanel.position.x + MainPanel.size.width > RightPanel.position.x) |
|
x -= MainPanel.position.x + MainPanel.size.width - RightPanel.position.x; |
|
} |
|
|
|
Player &myPlayer = *MyPlayer; |
|
x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HEAD], x, 3); |
|
x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_CHEST], x, 2); |
|
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 = GetPauseTRN(); |
|
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 DrawGoldSplit(const Surface &out, int amount) |
|
{ |
|
const int dialogX = 30; |
|
|
|
ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { dialogX, 178 }), (*pGBoxBuff)[0]); |
|
|
|
const std::string description = fmt::format( |
|
fmt::runtime(ngettext( |
|
/* TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold.*/ |
|
"You have {:s} gold piece. How many do you want to remove?", |
|
"You have {:s} gold pieces. How many do you want to remove?", |
|
initialDropGoldValue)), |
|
FormatInteger(initialDropGoldValue)); |
|
|
|
// Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words |
|
const std::string wrapped = WordWrapString(description, 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); |
|
|
|
std::string value; |
|
if (amount > 0) { |
|
value = StrCat(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, value, GetPanelPosition(UiPanels::Inventory, { dialogX + 37, 128 }), UiFlags::ColorWhite | UiFlags::PentaCursor); |
|
} |
|
|
|
void control_drop_gold(SDL_Keycode vkey) |
|
{ |
|
Player &myPlayer = *MyPlayer; |
|
|
|
if (myPlayer._pHitPoints >> 6 <= 0) { |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
return; |
|
} |
|
|
|
if (vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) { |
|
if (dropGoldValue > 0) |
|
RemoveGold(myPlayer, initialDropGoldIndex); |
|
CloseGoldDrop(); |
|
} else if (vkey == SDLK_ESCAPE) { |
|
CloseGoldDrop(); |
|
dropGoldValue = 0; |
|
} else if (vkey == SDLK_BACKSPACE) { |
|
dropGoldValue = dropGoldValue / 10; |
|
} |
|
} |
|
|
|
void DrawTalkPan(const Surface &out) |
|
{ |
|
if (!talkflag) |
|
return; |
|
|
|
const Point mainPanelPosition = GetMainPanel().position; |
|
|
|
DrawPanelBox(out, MakeSdlRect(175, sgbPlrTalkTbl + 20, 294, 5), mainPanelPosition + Displacement { 175, 4 }); |
|
int off = 0; |
|
for (int i = 293; i > 283; off++, i--) { |
|
DrawPanelBox(out, MakeSdlRect((off / 2) + 175, sgbPlrTalkTbl + off + 25, i, 1), mainPanelPosition + Displacement { (off / 2) + 175, off + 9 }); |
|
} |
|
DrawPanelBox(out, MakeSdlRect(185, sgbPlrTalkTbl + 35, 274, 30), mainPanelPosition + Displacement { 185, 19 }); |
|
DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + 65, 284, 5), mainPanelPosition + Displacement { 180, 49 }); |
|
for (int i = 0; i < 10; i++) { |
|
DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + i + 70, i + 284, 1), mainPanelPosition + Displacement { 180, i + 54 }); |
|
} |
|
DrawPanelBox(out, MakeSdlRect(170, sgbPlrTalkTbl + 80, 310, 55), mainPanelPosition + Displacement { 170, 64 }); |
|
|
|
int x = mainPanelPosition.x + 200; |
|
int y = mainPanelPosition.y + 10; |
|
|
|
const uint32_t len = DrawString(out, TalkMessage, { { x, y }, { 250, 39 } }, UiFlags::ColorWhite | UiFlags::PentaCursor, 1, 13); |
|
TalkMessage[std::min<size_t>(len, sizeof(TalkMessage) - 1)] = '\0'; |
|
|
|
x += 46; |
|
int talkBtn = 0; |
|
for (size_t i = 0; i < Players.size(); i++) { |
|
Player &player = Players[i]; |
|
if (&player == MyPlayer) |
|
continue; |
|
|
|
UiFlags color = player.friendlyMode ? UiFlags::ColorWhitegold : UiFlags::ColorRed; |
|
const Point talkPanPosition = mainPanelPosition + Displacement { 172, 84 + 18 * talkBtn }; |
|
if (WhisperList[i]) { |
|
if (TalkButtonsDown[talkBtn]) { |
|
ClxDraw(out, talkPanPosition, (*talkButtons)[talkBtn != 0 ? 3 : 2]); |
|
RenderClxSprite(out, (*TalkButton)[2], talkPanPosition + Displacement { 4, -15 }); |
|
} |
|
} else { |
|
int nCel = talkBtn != 0 ? 1 : 0; |
|
if (TalkButtonsDown[talkBtn]) |
|
nCel += 4; |
|
ClxDraw(out, talkPanPosition, (*talkButtons)[nCel]); |
|
RenderClxSprite(out, (*TalkButton)[TalkButtonsDown[talkBtn] ? 1 : 0], talkPanPosition + Displacement { 4, -15 }); |
|
} |
|
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; |
|
|
|
const Point mainPanelPosition = GetMainPanel().position; |
|
|
|
if (MousePosition.x < 172 + mainPanelPosition.x) |
|
return false; |
|
if (MousePosition.y < 69 + mainPanelPosition.y) |
|
return false; |
|
if (MousePosition.x > 233 + mainPanelPosition.x) |
|
return false; |
|
if (MousePosition.y > 123 + mainPanelPosition.y) |
|
return false; |
|
|
|
for (bool &talkButtonDown : TalkButtonsDown) { |
|
talkButtonDown = false; |
|
} |
|
|
|
TalkButtonsDown[(MousePosition.y - (69 + mainPanelPosition.y)) / 18] = true; |
|
|
|
return true; |
|
} |
|
|
|
void control_release_talk_btn() |
|
{ |
|
if (!talkflag) |
|
return; |
|
|
|
for (bool &talkButtonDown : TalkButtonsDown) |
|
talkButtonDown = false; |
|
|
|
const Point mainPanelPosition = GetMainPanel().position; |
|
|
|
if (MousePosition.x < 172 + mainPanelPosition.x || MousePosition.y < 69 + mainPanelPosition.y || MousePosition.x > 233 + mainPanelPosition.x || MousePosition.y > 123 + mainPanelPosition.y) |
|
return; |
|
|
|
int off = (MousePosition.y - (69 + mainPanelPosition.y)) / 18; |
|
|
|
size_t playerId = 0; |
|
for (; playerId < Players.size() && off != -1; ++playerId) { |
|
if (playerId != MyPlayerId) |
|
off--; |
|
} |
|
if (playerId > 0 && playerId <= Players.size()) |
|
WhisperList[playerId - 1] = !WhisperList[playerId - 1]; |
|
} |
|
|
|
void control_type_message() |
|
{ |
|
if (!IsChatAvailable()) |
|
return; |
|
|
|
talkflag = true; |
|
SDL_Rect rect = MakeSdlRect(GetMainPanel().position.x + 200, GetMainPanel().position.y + 22, 0, 27); |
|
SDL_SetTextInputRect(&rect); |
|
TalkMessage[0] = '\0'; |
|
for (bool &talkButtonDown : TalkButtonsDown) { |
|
talkButtonDown = false; |
|
} |
|
sgbPlrTalkTbl = GetMainPanel().size.height + 16; |
|
RedrawEverything(); |
|
TalkSaveIndex = NextTalkSave; |
|
SDL_StartTextInput(); |
|
} |
|
|
|
void control_reset_talk() |
|
{ |
|
talkflag = false; |
|
SDL_StopTextInput(); |
|
sgbPlrTalkTbl = 0; |
|
RedrawEverything(); |
|
} |
|
|
|
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(SDL_Keycode vkey) |
|
{ |
|
if (!IsChatAvailable()) |
|
return false; |
|
if (!talkflag) |
|
return false; |
|
|
|
switch (vkey) { |
|
case SDLK_ESCAPE: |
|
control_reset_talk(); |
|
return true; |
|
case SDLK_RETURN: |
|
case SDLK_KP_ENTER: |
|
ControlPressEnter(); |
|
return true; |
|
case SDLK_BACKSPACE: |
|
TalkMessage[FindLastUtf8Symbols(TalkMessage)] = '\0'; |
|
return true; |
|
case SDLK_DOWN: |
|
ControlUpDown(1); |
|
return true; |
|
case SDLK_UP: |
|
ControlUpDown(-1); |
|
return true; |
|
default: |
|
return vkey >= SDLK_SPACE && vkey <= SDLK_z; |
|
} |
|
} |
|
|
|
void DiabloHotkeyMsg(uint32_t dwMsg) |
|
{ |
|
if (!IsChatAvailable()) { |
|
return; |
|
} |
|
|
|
assert(dwMsg < QUICK_MESSAGE_OPTIONS); |
|
|
|
for (auto &msg : sgOptions.Chat.szHotKeyMsgs[dwMsg]) { |
|
|
|
#ifdef _DEBUG |
|
if (CheckDebugTextCommand(msg)) |
|
continue; |
|
#endif |
|
if (CheckTextCommand(msg)) |
|
continue; |
|
char charMsg[MAX_SEND_STR_LEN]; |
|
CopyUtf8(charMsg, msg, sizeof(charMsg)); |
|
NetSendCmdString(0xFFFFFF, charMsg); |
|
} |
|
} |
|
|
|
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
|
|
|