225 changed files with 19212 additions and 6807 deletions
@ -1,7 +1,7 @@
|
||||
#pragma once |
||||
|
||||
namespace devilution { |
||||
|
||||
void UiSelStartUpGameOption(); |
||||
|
||||
} // namespace devilution
|
||||
#pragma once |
||||
|
||||
namespace devilution { |
||||
|
||||
void UiSelStartUpGameOption(); |
||||
|
||||
} // namespace devilution
|
||||
|
||||
@ -0,0 +1,319 @@
|
||||
#include "control_chat.hpp" |
||||
#include "control.hpp" |
||||
#include "control_panel.hpp" |
||||
|
||||
#include "control/control_chat_commands.hpp" |
||||
#include "engine/backbuffer_state.hpp" |
||||
#include "engine/render/clx_render.hpp" |
||||
#include "options.h" |
||||
#include "panels/console.hpp" |
||||
#include "panels/mainpanel.hpp" |
||||
#include "quick_messages.hpp" |
||||
#include "utils/display.h" |
||||
#include "utils/sdl_compat.h" |
||||
#include "utils/str_cat.hpp" |
||||
|
||||
namespace devilution { |
||||
|
||||
std::optional<TextInputState> ChatInputState; |
||||
char TalkMessage[MAX_SEND_STR_LEN]; |
||||
bool TalkButtonsDown[3]; |
||||
int sgbPlrTalkTbl; |
||||
bool WhisperList[MAX_PLRS]; |
||||
OptionalOwnedClxSpriteList talkButtons; |
||||
|
||||
namespace { |
||||
|
||||
char TalkSave[8][MAX_SEND_STR_LEN]; |
||||
uint8_t TalkSaveIndex; |
||||
uint8_t NextTalkSave; |
||||
TextInputCursorState ChatCursor; |
||||
|
||||
int MuteButtons = 3; |
||||
int MuteButtonPadding = 2; |
||||
Rectangle MuteButtonRect { { 172, 69 }, { 61, 16 } }; |
||||
|
||||
void ResetChatMessage() |
||||
{ |
||||
if (CheckChatCommand(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) { |
||||
ResetChatMessage(); |
||||
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]); |
||||
*BufCopy(TalkSave[talkSave], ChatInputState->value()) = '\0'; |
||||
} |
||||
} |
||||
TalkMessage[0] = '\0'; |
||||
TalkSaveIndex = NextTalkSave; |
||||
} |
||||
ResetChat(); |
||||
} |
||||
|
||||
void ControlUpDown(int v) |
||||
{ |
||||
for (int i = 0; i < 8; i++) { |
||||
TalkSaveIndex = (v + TalkSaveIndex) & 7; |
||||
if (TalkSave[TalkSaveIndex][0] != 0) { |
||||
ChatInputState->assign(TalkSave[TalkSaveIndex]); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
} // namespace
|
||||
|
||||
void DrawChatBox(const Surface &out) |
||||
{ |
||||
if (!ChatFlag) |
||||
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; |
||||
const int y = mainPanelPosition.y + 10; |
||||
|
||||
const uint32_t len = DrawString(out, TalkMessage, { { x, y }, { 250, 39 } }, |
||||
{ |
||||
.flags = UiFlags::ColorWhite | UiFlags::PentaCursor, |
||||
.lineHeight = 13, |
||||
.cursorPosition = static_cast<int>(ChatCursor.position), |
||||
.highlightRange = { static_cast<int>(ChatCursor.selection.begin), static_cast<int>(ChatCursor.selection.end) }, |
||||
}); |
||||
ChatInputState->truncate(len); |
||||
|
||||
x += 46; |
||||
int talkBtn = 0; |
||||
for (size_t i = 0; i < Players.size(); i++) { |
||||
Player &player = Players[i]; |
||||
if (&player == MyPlayer) |
||||
continue; |
||||
|
||||
const UiFlags color = player.friendlyMode ? UiFlags::ColorWhitegold : UiFlags::ColorRed; |
||||
const Point talkPanPosition = mainPanelPosition + Displacement { 172, 84 + 18 * talkBtn }; |
||||
if (WhisperList[i]) { |
||||
// the normal (unpressed) voice button is pre-rendered on the panel, only need to draw over it when the button is held
|
||||
if (TalkButtonsDown[talkBtn]) { |
||||
const unsigned spriteIndex = talkBtn == 0 ? 2 : 3; // the first button sprite includes a tip from the devils wing so is different to the rest.
|
||||
ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]); |
||||
|
||||
// Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting
|
||||
// the first button be treated the same as the other two further down the panel.
|
||||
RenderClxSprite(out, (*TalkButton)[2], talkPanPosition + Displacement { 4, -15 }); |
||||
} |
||||
} else { |
||||
unsigned spriteIndex = talkBtn == 0 ? 0 : 1; // the first button sprite includes a tip from the devils wing so is different to the rest.
|
||||
if (TalkButtonsDown[talkBtn]) |
||||
spriteIndex += 4; // held button sprites are at index 4 and 5 (with and without wingtip respectively)
|
||||
ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]); |
||||
|
||||
// Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting
|
||||
// the first button be treated the same as the other two further down the panel.
|
||||
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 } }, { .flags = color }); |
||||
} |
||||
|
||||
talkBtn++; |
||||
} |
||||
} |
||||
|
||||
bool CheckMuteButton() |
||||
{ |
||||
if (!ChatFlag) |
||||
return false; |
||||
|
||||
Rectangle buttons = MuteButtonRect; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, buttons); |
||||
|
||||
buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding); |
||||
|
||||
if (!buttons.contains(MousePosition)) |
||||
return false; |
||||
|
||||
for (bool &talkButtonDown : TalkButtonsDown) { |
||||
talkButtonDown = false; |
||||
} |
||||
|
||||
const Point mainPanelPosition = GetMainPanel().position; |
||||
|
||||
TalkButtonsDown[(MousePosition.y - (69 + mainPanelPosition.y)) / 18] = true; |
||||
|
||||
return true; |
||||
} |
||||
|
||||
void CheckMuteButtonUp() |
||||
{ |
||||
if (!ChatFlag) |
||||
return; |
||||
|
||||
for (bool &talkButtonDown : TalkButtonsDown) |
||||
talkButtonDown = false; |
||||
|
||||
Rectangle buttons = MuteButtonRect; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, buttons); |
||||
|
||||
buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding); |
||||
|
||||
if (!buttons.contains(MousePosition)) |
||||
return; |
||||
|
||||
int off = (MousePosition.y - buttons.position.y) / (MuteButtonRect.size.height + MuteButtonPadding); |
||||
|
||||
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 TypeChatMessage() |
||||
{ |
||||
if (!IsChatAvailable()) |
||||
return; |
||||
|
||||
ChatFlag = true; |
||||
TalkMessage[0] = '\0'; |
||||
ChatInputState.emplace(TextInputState::Options { |
||||
.value = TalkMessage, |
||||
.cursor = &ChatCursor, |
||||
.maxLength = sizeof(TalkMessage) - 1 }); |
||||
for (bool &talkButtonDown : TalkButtonsDown) { |
||||
talkButtonDown = false; |
||||
} |
||||
sgbPlrTalkTbl = GetMainPanel().size.height + PanelPaddingHeight; |
||||
RedrawEverything(); |
||||
TalkSaveIndex = NextTalkSave; |
||||
|
||||
SDL_Rect rect = MakeSdlRect(GetMainPanel().position.x + 200, GetMainPanel().position.y + 22, 0, 27); |
||||
SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); |
||||
SDLC_StartTextInput(ghMainWnd); |
||||
} |
||||
|
||||
void ResetChat() |
||||
{ |
||||
ChatFlag = false; |
||||
SDLC_StopTextInput(ghMainWnd); |
||||
ChatCursor = {}; |
||||
ChatInputState = std::nullopt; |
||||
sgbPlrTalkTbl = 0; |
||||
RedrawEverything(); |
||||
} |
||||
|
||||
bool IsChatActive() |
||||
{ |
||||
if (!IsChatAvailable()) |
||||
return false; |
||||
|
||||
if (!ChatFlag) |
||||
return false; |
||||
|
||||
return true; |
||||
} |
||||
|
||||
bool CheckKeypress(SDL_Keycode vkey) |
||||
{ |
||||
if (!IsChatAvailable()) |
||||
return false; |
||||
if (!ChatFlag) |
||||
return false; |
||||
|
||||
switch (vkey) { |
||||
case SDLK_ESCAPE: |
||||
ResetChat(); |
||||
return true; |
||||
case SDLK_RETURN: |
||||
case SDLK_KP_ENTER: |
||||
ControlPressEnter(); |
||||
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) |
||||
{ |
||||
assert(dwMsg < QuickMessages.size()); |
||||
|
||||
#ifdef _DEBUG |
||||
constexpr std::string_view LuaPrefix = "/lua "; |
||||
for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) { |
||||
if (!msg.starts_with(LuaPrefix)) continue; |
||||
InitConsole(); |
||||
RunInConsole(std::string_view(msg).substr(LuaPrefix.size())); |
||||
} |
||||
#endif |
||||
|
||||
if (!IsChatAvailable()) { |
||||
return; |
||||
} |
||||
|
||||
for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) { |
||||
#ifdef _DEBUG |
||||
if (msg.starts_with(LuaPrefix)) continue; |
||||
#endif |
||||
char charMsg[MAX_SEND_STR_LEN]; |
||||
CopyUtf8(charMsg, msg, sizeof(charMsg)); |
||||
NetSendCmdString(0xFFFFFF, charMsg); |
||||
} |
||||
} |
||||
|
||||
bool IsChatAvailable() |
||||
{ |
||||
return gbIsMultiplayer; |
||||
} |
||||
|
||||
bool HandleTalkTextInputEvent(const SDL_Event &event) |
||||
{ |
||||
return HandleInputEvent(event, ChatInputState); |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,51 @@
|
||||
#pragma once |
||||
|
||||
#include <optional> |
||||
#include <string_view> |
||||
|
||||
#include "DiabloUI/text_input.hpp" |
||||
#include "engine/clx_sprite.hpp" |
||||
#include "msg.h" |
||||
#include "multi.h" |
||||
|
||||
#ifdef USE_SDL3 |
||||
#include <SDL3/SDL_events.h> |
||||
#include <SDL3/SDL_keyboard.h> |
||||
#include <SDL3/SDL_keycode.h> |
||||
#include <SDL3/SDL_rect.h> |
||||
#else |
||||
#include <SDL.h> |
||||
|
||||
#ifdef USE_SDL1 |
||||
#include "utils/sdl2_to_1_2_backports.h" |
||||
#endif |
||||
#endif |
||||
|
||||
namespace devilution { |
||||
|
||||
extern OptionalOwnedClxSpriteList talkButtons; |
||||
extern std::optional<TextInputState> ChatInputState; |
||||
extern char TalkMessage[MAX_SEND_STR_LEN]; |
||||
extern bool TalkButtonsDown[3]; |
||||
extern int sgbPlrTalkTbl; |
||||
extern bool WhisperList[MAX_PLRS]; |
||||
|
||||
bool CheckChatCommand(std::string_view text); |
||||
|
||||
template <typename InputStateType> |
||||
bool HandleInputEvent(const SDL_Event &event, std::optional<InputStateType> &inputState) |
||||
{ |
||||
if (!inputState) { |
||||
return false; // No input state to handle
|
||||
} |
||||
|
||||
if constexpr (std::is_same_v<InputStateType, TextInputState>) { |
||||
return HandleTextInputEvent(event, *inputState); |
||||
} else if constexpr (std::is_same_v<InputStateType, NumberInputState>) { |
||||
return HandleNumberInputEvent(event, *inputState); |
||||
} |
||||
|
||||
return false; // Unknown input state type
|
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,280 @@
|
||||
#include "control_chat_commands.hpp" |
||||
#include "control.hpp" |
||||
|
||||
#include "diablo_msg.hpp" |
||||
#include "engine/backbuffer_state.hpp" |
||||
#include "inv.h" |
||||
#include "levels/setmaps.h" |
||||
#include "storm/storm_net.hpp" |
||||
#include "utils/algorithm/container.hpp" |
||||
#include "utils/log.hpp" |
||||
#include "utils/parse_int.hpp" |
||||
#include "utils/str_case.hpp" |
||||
#include "utils/str_cat.hpp" |
||||
|
||||
#ifdef _DEBUG |
||||
#include "debug.h" |
||||
#endif |
||||
|
||||
namespace devilution { |
||||
|
||||
namespace { |
||||
|
||||
struct TextCmdItem { |
||||
const std::string text; |
||||
const std::string description; |
||||
const std::string requiredParameter; |
||||
std::string (*actionProc)(const std::string_view); |
||||
}; |
||||
|
||||
extern std::vector<TextCmdItem> TextCmdList; |
||||
|
||||
std::string TextCmdHelp(const std::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 = c_find_if(TextCmdList, [&](const TextCmdItem &elem) { return elem.text == parameter; }); |
||||
if (textCmdIterator == TextCmdList.end()) |
||||
return StrCat(_("Command "), parameter, _(" is unknown.")); |
||||
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], ")"); |
||||
} |
||||
} |
||||
|
||||
std::string TextCmdArena(const std::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; |
||||
} |
||||
|
||||
const ParseIntResult<int> parsedParam = ParseInt<int>(parameter, /*min=*/0); |
||||
const _setlevels arenaLevel = parsedParam.has_value() ? static_cast<_setlevels>(parsedParam.value() - 1 + SL_FIRST_ARENA) : _setlevels::SL_NONE; |
||||
if (!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 = GetArenaLevelType(arenaLevel); |
||||
StartNewLvl(*MyPlayer, WM_DIABSETLVL, arenaLevel); |
||||
return ret; |
||||
} |
||||
|
||||
std::string TextCmdArenaPot(const std::string_view parameter) |
||||
{ |
||||
std::string ret; |
||||
if (!gbIsMultiplayer) { |
||||
StrAppend(ret, _("Arenas are only supported in multiplayer.")); |
||||
return ret; |
||||
} |
||||
const int numPots = ParseInt<int>(parameter, /*min=*/1).value_or(1); |
||||
|
||||
Player &myPlayer = *MyPlayer; |
||||
|
||||
for (int potNumber = numPots; potNumber > 0; potNumber--) { |
||||
Item item {}; |
||||
InitializeItem(item, IDI_ARENAPOT); |
||||
GenerateNewSeed(item); |
||||
item.updateRequiredStatsCacheForPlayer(myPlayer); |
||||
|
||||
if (!AutoPlaceItemInBelt(myPlayer, item, true, true) && !AutoPlaceItemInInventory(myPlayer, item, true)) { |
||||
break; // inventory is full
|
||||
} |
||||
} |
||||
|
||||
return ret; |
||||
} |
||||
|
||||
std::string TextCmdInspect(const std::string_view parameter) |
||||
{ |
||||
std::string ret; |
||||
if (!gbIsMultiplayer) { |
||||
StrAppend(ret, _("Inspecting only supported in multiplayer.")); |
||||
return ret; |
||||
} |
||||
|
||||
if (parameter.empty()) { |
||||
StrAppend(ret, _("Stopped inspecting players.")); |
||||
InspectPlayer = MyPlayer; |
||||
return ret; |
||||
} |
||||
|
||||
const std::string param = AsciiStrToLower(parameter); |
||||
auto it = c_find_if(Players, [¶m](const Player &player) { |
||||
return AsciiStrToLower(player._pName) == param; |
||||
}); |
||||
if (it == Players.end()) { |
||||
it = c_find_if(Players, [¶m](const Player &player) { |
||||
return AsciiStrToLower(player._pName).find(param) != std::string::npos; |
||||
}); |
||||
} |
||||
if (it == Players.end()) { |
||||
StrAppend(ret, _("No players found with such a name")); |
||||
return ret; |
||||
} |
||||
|
||||
Player &player = *it; |
||||
InspectPlayer = &player; |
||||
StrAppend(ret, _("Inspecting player: ")); |
||||
StrAppend(ret, player._pName); |
||||
OpenCharPanel(); |
||||
if (!SpellbookFlag) |
||||
invflag = true; |
||||
RedrawEverything(); |
||||
return ret; |
||||
} |
||||
|
||||
bool IsQuestEnabled(const Quest &quest) |
||||
{ |
||||
switch (quest._qidx) { |
||||
case Q_FARMER: |
||||
return gbIsHellfire && !sgGameInitInfo.bCowQuest; |
||||
case Q_JERSEY: |
||||
return gbIsHellfire && sgGameInitInfo.bCowQuest; |
||||
case Q_GIRL: |
||||
return gbIsHellfire && sgGameInitInfo.bTheoQuest; |
||||
case Q_CORNSTN: |
||||
return gbIsHellfire && !gbIsMultiplayer; |
||||
case Q_GRAVE: |
||||
case Q_DEFILER: |
||||
case Q_NAKRUL: |
||||
return gbIsHellfire; |
||||
case Q_TRADER: |
||||
return false; |
||||
default: |
||||
return quest._qactive != QUEST_NOTAVAIL; |
||||
} |
||||
} |
||||
|
||||
std::string TextCmdLevelSeed(const std::string_view parameter) |
||||
{ |
||||
const std::string_view levelType = setlevel ? "set level" : "dungeon level"; |
||||
|
||||
char gameId[] = { |
||||
static_cast<char>((sgGameInitInfo.programid >> 24) & 0xFF), |
||||
static_cast<char>((sgGameInitInfo.programid >> 16) & 0xFF), |
||||
static_cast<char>((sgGameInitInfo.programid >> 8) & 0xFF), |
||||
static_cast<char>(sgGameInitInfo.programid & 0xFF), |
||||
'\0' |
||||
}; |
||||
|
||||
const std::string_view mode = gbIsMultiplayer ? "MP" : "SP"; |
||||
const std::string_view questPool = UseMultiplayerQuests() ? "MP" : "Full"; |
||||
|
||||
uint32_t questFlags = 0; |
||||
for (const Quest &quest : Quests) { |
||||
questFlags <<= 1; |
||||
if (IsQuestEnabled(quest)) |
||||
questFlags |= 1; |
||||
} |
||||
|
||||
return StrCat( |
||||
"Seedinfo for ", levelType, " ", currlevel, "\n", |
||||
"seed: ", DungeonSeeds[currlevel], "\n", |
||||
#ifdef _DEBUG |
||||
"Mid1: ", glMid1Seed[currlevel], "\n", |
||||
"Mid2: ", glMid2Seed[currlevel], "\n", |
||||
"Mid3: ", glMid3Seed[currlevel], "\n", |
||||
"End: ", glEndSeed[currlevel], "\n", |
||||
#endif |
||||
"\n", |
||||
gameId, " ", mode, "\n", |
||||
questPool, " quests: ", questFlags, "\n", |
||||
"Storybook: ", DungeonSeeds[16]); |
||||
} |
||||
|
||||
std::string TextCmdPing(const std::string_view parameter) |
||||
{ |
||||
std::string ret; |
||||
const std::string param = AsciiStrToLower(parameter); |
||||
auto it = c_find_if(Players, [¶m](const Player &player) { |
||||
return AsciiStrToLower(player._pName) == param; |
||||
}); |
||||
if (it == Players.end()) { |
||||
it = c_find_if(Players, [¶m](const Player &player) { |
||||
return AsciiStrToLower(player._pName).find(param) != std::string::npos; |
||||
}); |
||||
} |
||||
if (it == Players.end()) { |
||||
StrAppend(ret, _("No players found with such a name")); |
||||
return ret; |
||||
} |
||||
|
||||
Player &player = *it; |
||||
DvlNetLatencies latencies = DvlNet_GetLatencies(player.getId()); |
||||
|
||||
StrAppend(ret, fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Latency statistics for {:s}:")), player.name())); |
||||
|
||||
StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency)); |
||||
|
||||
if (latencies.providerLatency) { |
||||
if (latencies.isRelayed && *latencies.isRelayed) { |
||||
StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency)); |
||||
} else { |
||||
StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency)); |
||||
} |
||||
} |
||||
|
||||
return ret; |
||||
} |
||||
|
||||
std::vector<TextCmdItem> TextCmdList = { |
||||
{ "/help", N_("Prints help overview or help for a specific command."), N_("[command]"), &TextCmdHelp }, |
||||
{ "/arena", N_("Enter a PvP Arena."), N_("<arena-number>"), &TextCmdArena }, |
||||
{ "/arenapot", N_("Gives Arena Potions."), N_("<number>"), &TextCmdArenaPot }, |
||||
{ "/inspect", N_("Inspects stats and equipment of another player."), N_("<player name>"), &TextCmdInspect }, |
||||
{ "/seedinfo", N_("Show seed infos for current level."), "", &TextCmdLevelSeed }, |
||||
{ "/ping", N_("Show latency statistics for another player."), N_("<player name>"), &TextCmdPing }, |
||||
}; |
||||
|
||||
} // namespace
|
||||
|
||||
bool CheckChatCommand(const std::string_view text) |
||||
{ |
||||
if (text.size() < 1 || text[0] != '/') |
||||
return false; |
||||
|
||||
auto textCmdIterator = c_find_if(TextCmdList, [&](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; |
||||
} |
||||
|
||||
const TextCmdItem &textCmd = *textCmdIterator; |
||||
std::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; |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,9 @@
|
||||
#pragma once |
||||
|
||||
#include <string_view> |
||||
|
||||
namespace devilution { |
||||
|
||||
bool CheckChatCommand(std::string_view text); |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,149 @@
|
||||
#include "control_flasks.hpp" |
||||
#include "control.hpp" |
||||
|
||||
#include "engine/surface.hpp" |
||||
#include "utils/str_cat.hpp" |
||||
|
||||
namespace devilution { |
||||
|
||||
std::optional<OwnedSurface> pLifeBuff; |
||||
std::optional<OwnedSurface> pManaBuff; |
||||
|
||||
namespace { |
||||
|
||||
Rectangle FlaskTopRect { { 11, 3 }, { 62, 13 } }; |
||||
Rectangle FlaskBottomRect { { 0, 16 }, { 88, 69 } }; |
||||
|
||||
/**
|
||||
* 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 flask cel. |
||||
* @param targetPosition Target buffer coordinate. |
||||
*/ |
||||
void DrawFlaskAbovePanel(const Surface &out, const Surface &celBuf, Point targetPosition) |
||||
{ |
||||
out.BlitFromSkipColorIndexZero(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.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 81) |
||||
*/ |
||||
void DrawFlaskUpper(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer) |
||||
{ |
||||
const Rectangle &rect = FlaskTopRect; |
||||
const int emptyRows = std::clamp(81 - fillPer, 0, rect.size.height); |
||||
const int filledRows = rect.size.height - emptyRows; |
||||
|
||||
// Draw the empty part of the flask
|
||||
DrawFlaskAbovePanel(out, |
||||
sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, rect.size.height), |
||||
GetMainPanel().position + Displacement { offset, -rect.size.height }); |
||||
|
||||
// Draw the filled part of the flask over the empty part
|
||||
if (filledRows > 0) { |
||||
DrawFlaskAbovePanel(out, |
||||
BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows), |
||||
GetMainPanel().position + Displacement { offset, -rect.size.height + emptyRows }); |
||||
} |
||||
} |
||||
|
||||
/**
|
||||
* 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 celBuf Buffer of the flask cel. |
||||
* @param targetPosition Target buffer coordinate. |
||||
*/ |
||||
void DrawFlaskOnPanel(const Surface &out, const Surface &celBuf, Point targetPosition) |
||||
{ |
||||
out.BlitFrom(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition); |
||||
} |
||||
|
||||
/**
|
||||
* @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) |
||||
* @param drawFilledPortion Indicates whether to draw the filled portion of the flask |
||||
*/ |
||||
void DrawFlaskLower(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer, bool drawFilledPortion) |
||||
{ |
||||
const Rectangle &rect = FlaskBottomRect; |
||||
const int filledRows = std::clamp(fillPer, 0, rect.size.height); |
||||
const int emptyRows = rect.size.height - filledRows; |
||||
|
||||
// Draw the empty part of the flask
|
||||
if (emptyRows > 0) { |
||||
DrawFlaskOnPanel(out, |
||||
sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, emptyRows), |
||||
GetMainPanel().position + Displacement { offset, 0 }); |
||||
} |
||||
|
||||
// Draw the filled part of the flask
|
||||
if (drawFilledPortion && filledRows > 0) { |
||||
DrawFlaskOnPanel(out, |
||||
BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows), |
||||
GetMainPanel().position + Displacement { offset, emptyRows }); |
||||
} |
||||
} |
||||
|
||||
} // namespace
|
||||
|
||||
void DrawLifeFlaskUpper(const Surface &out) |
||||
{ |
||||
constexpr int LifeFlaskUpperOffset = 107; |
||||
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, bool drawFilledPortion) |
||||
{ |
||||
constexpr int LifeFlaskLowerOffset = 96; |
||||
DrawFlaskLower(out, *pLifeBuff, LifeFlaskLowerOffset, MyPlayer->_pHPPer, drawFilledPortion); |
||||
} |
||||
|
||||
void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion) |
||||
{ |
||||
constexpr int ManaFlaskLowerOffset = 464; |
||||
DrawFlaskLower(out, *pManaBuff, ManaFlaskLowerOffset, MyPlayer->_pManaPer, drawFilledPortion); |
||||
} |
||||
|
||||
void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue) |
||||
{ |
||||
const UiFlags color = (currValue > 0 ? (currValue == maxValue ? UiFlags::ColorGold : UiFlags::ColorWhite) : UiFlags::ColorRed); |
||||
|
||||
auto drawStringWithShadow = [out, color](std::string_view text, Point pos) { |
||||
DrawString(out, text, pos + Displacement { -1, -1 }, |
||||
{ .flags = UiFlags::ColorBlack | UiFlags::KerningFitSpacing, .spacing = 0 }); |
||||
DrawString(out, text, pos, |
||||
{ .flags = color | UiFlags::KerningFitSpacing, .spacing = 0 }); |
||||
}; |
||||
|
||||
const 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 UpdateLifeManaPercent() |
||||
{ |
||||
MyPlayer->UpdateManaPercentage(); |
||||
MyPlayer->UpdateHitPointPercentage(); |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,12 @@
|
||||
#pragma once |
||||
|
||||
#include <optional> |
||||
|
||||
#include "engine/surface.hpp" |
||||
|
||||
namespace devilution { |
||||
|
||||
extern std::optional<OwnedSurface> pLifeBuff; |
||||
extern std::optional<OwnedSurface> pManaBuff; |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,141 @@
|
||||
#include "control.hpp" |
||||
#include "control_chat.hpp" |
||||
|
||||
#include "DiabloUI/text_input.hpp" |
||||
#include "engine/render/clx_render.hpp" |
||||
#include "inv.h" |
||||
#include "utils/display.h" |
||||
#include "utils/format_int.hpp" |
||||
#include "utils/log.hpp" |
||||
#include "utils/sdl_compat.h" |
||||
|
||||
namespace devilution { |
||||
|
||||
bool DropGoldFlag; |
||||
TextInputCursorState GoldDropCursor; |
||||
char GoldDropText[21]; |
||||
|
||||
namespace { |
||||
|
||||
int8_t GoldDropInvIndex; |
||||
std::optional<NumberInputState> GoldDropInputState; |
||||
|
||||
void RemoveGold(Player &player, int goldIndex, int amount) |
||||
{ |
||||
const int gi = goldIndex - INVITEM_INV_FIRST; |
||||
player.InvList[gi]._ivalue -= amount; |
||||
if (player.InvList[gi]._ivalue > 0) { |
||||
SetPlrHandGoldCurs(player.InvList[gi]); |
||||
NetSyncInvItem(player, gi); |
||||
} else { |
||||
player.RemoveInvItem(gi); |
||||
} |
||||
|
||||
MakeGoldStack(player.HoldItem, amount); |
||||
NewCursor(player.HoldItem); |
||||
|
||||
player._pGold = CalculateGold(player); |
||||
} |
||||
|
||||
int GetGoldDropMax() |
||||
{ |
||||
return GoldDropInputState->max(); |
||||
} |
||||
|
||||
} // namespace
|
||||
|
||||
void DrawGoldSplit(const Surface &out) |
||||
{ |
||||
const int dialogX = 30; |
||||
|
||||
ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { dialogX, 178 }), (*GoldBoxBuffer)[0]); |
||||
|
||||
const std::string_view amountText = GoldDropText; |
||||
const TextInputCursorState &cursor = GoldDropCursor; |
||||
const int max = GetGoldDropMax(); |
||||
|
||||
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?", |
||||
max)), |
||||
FormatInteger(max)); |
||||
|
||||
// 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 } }, |
||||
{ .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter, .lineHeight = 17 }); |
||||
|
||||
// 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, amountText, GetPanelPosition(UiPanels::Inventory, { dialogX + 37, 128 }), |
||||
{ |
||||
.flags = UiFlags::ColorWhite | UiFlags::PentaCursor, |
||||
.cursorPosition = static_cast<int>(cursor.position), |
||||
.highlightRange = { static_cast<int>(cursor.selection.begin), static_cast<int>(cursor.selection.end) }, |
||||
}); |
||||
} |
||||
|
||||
void control_drop_gold(SDL_Keycode vkey) |
||||
{ |
||||
Player &myPlayer = *MyPlayer; |
||||
|
||||
if (myPlayer.hasNoLife()) { |
||||
CloseGoldDrop(); |
||||
return; |
||||
} |
||||
|
||||
switch (vkey) { |
||||
case SDLK_RETURN: |
||||
case SDLK_KP_ENTER: |
||||
if (const int value = GoldDropInputState->value(); value != 0) { |
||||
RemoveGold(myPlayer, GoldDropInvIndex, value); |
||||
} |
||||
CloseGoldDrop(); |
||||
break; |
||||
case SDLK_ESCAPE: |
||||
CloseGoldDrop(); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
} |
||||
|
||||
void OpenGoldDrop(int8_t invIndex, int max) |
||||
{ |
||||
DropGoldFlag = true; |
||||
GoldDropInvIndex = invIndex; |
||||
GoldDropText[0] = '\0'; |
||||
GoldDropInputState.emplace(NumberInputState::Options { |
||||
.textOptions { |
||||
.value = GoldDropText, |
||||
.cursor = &GoldDropCursor, |
||||
.maxLength = sizeof(GoldDropText) - 1, |
||||
}, |
||||
.min = 0, |
||||
.max = max, |
||||
}); |
||||
SDLC_StartTextInput(ghMainWnd); |
||||
} |
||||
|
||||
void CloseGoldDrop() |
||||
{ |
||||
if (!DropGoldFlag) |
||||
return; |
||||
SDLC_StopTextInput(ghMainWnd); |
||||
DropGoldFlag = false; |
||||
GoldDropInputState = std::nullopt; |
||||
GoldDropInvIndex = 0; |
||||
} |
||||
|
||||
bool HandleGoldDropTextInputEvent(const SDL_Event &event) |
||||
{ |
||||
return HandleInputEvent(event, GoldDropInputState); |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,425 @@
|
||||
#include "control.hpp" |
||||
#include "control_panel.hpp" |
||||
|
||||
#include "engine/render/primitive_render.hpp" |
||||
#include "inv.h" |
||||
#include "levels/trigs.h" |
||||
#include "panels/partypanel.hpp" |
||||
#include "qol/stash.h" |
||||
#include "qol/xpbar.h" |
||||
#include "towners.h" |
||||
#include "utils/algorithm/container.hpp" |
||||
#include "utils/format_int.hpp" |
||||
#include "utils/log.hpp" |
||||
#include "utils/screen_reader.hpp" |
||||
#include "utils/str_cat.hpp" |
||||
#include "utils/str_split.hpp" |
||||
|
||||
namespace devilution { |
||||
|
||||
StringOrView InfoString; |
||||
StringOrView FloatingInfoString; |
||||
|
||||
namespace { |
||||
|
||||
void PrintInfo(const Surface &out) |
||||
{ |
||||
if (ChatFlag) |
||||
return; |
||||
|
||||
const int space[] = { 18, 12, 6, 3, 0 }; |
||||
Rectangle infoBox = InfoBoxRect; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, infoBox); |
||||
|
||||
const auto newLineCount = static_cast<int>(c_count(InfoString.str(), '\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
|
||||
infoBox.position.y += spacing / 2; |
||||
|
||||
SpeakText(InfoString); |
||||
|
||||
DrawString(out, InfoString, infoBox, |
||||
{ |
||||
.flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::KerningFitSpacing, |
||||
.spacing = 2, |
||||
.lineHeight = lineHeight, |
||||
}); |
||||
} |
||||
|
||||
Rectangle GetFloatingInfoRect(const int lineHeight, const int textSpacing) |
||||
{ |
||||
// Calculate the width and height of the floating info box
|
||||
const std::string txt = std::string(FloatingInfoString); |
||||
|
||||
auto lines = SplitByChar(txt, '\n'); |
||||
const GameFontTables font = GameFont12; |
||||
int maxW = 0; |
||||
|
||||
for (const auto &line : lines) { |
||||
const int w = GetLineWidth(line, font, textSpacing, nullptr); |
||||
maxW = std::max(maxW, w); |
||||
} |
||||
|
||||
const auto lineCount = 1 + static_cast<int>(c_count(FloatingInfoString.str(), '\n')); |
||||
const int totalH = lineCount * lineHeight; |
||||
|
||||
const Player &player = *InspectPlayer; |
||||
|
||||
// 1) Equipment (Rect position)
|
||||
if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) { |
||||
const int slot = pcursinvitem - INVITEM_HEAD; |
||||
static constexpr Point equipLocal[] = { |
||||
{ 133, 59 }, |
||||
{ 48, 205 }, |
||||
{ 249, 205 }, |
||||
{ 205, 60 }, |
||||
{ 17, 160 }, |
||||
{ 248, 160 }, |
||||
{ 133, 160 }, |
||||
}; |
||||
|
||||
Point itemPosition = equipLocal[slot]; |
||||
auto &item = player.InvBody[slot]; |
||||
const Size frame = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM); |
||||
|
||||
if (slot == INVLOC_HAND_LEFT) { |
||||
itemPosition.x += frame.width == InventorySlotSizeInPixels.width |
||||
? InventorySlotSizeInPixels.width |
||||
: 0; |
||||
itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height |
||||
? 0 |
||||
: -InventorySlotSizeInPixels.height; |
||||
} else if (slot == INVLOC_HAND_RIGHT) { |
||||
itemPosition.x += frame.width == InventorySlotSizeInPixels.width |
||||
? (InventorySlotSizeInPixels.width - 1) |
||||
: 1; |
||||
itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height |
||||
? 0 |
||||
: -InventorySlotSizeInPixels.height; |
||||
} |
||||
|
||||
itemPosition.y++; // Align position to bottom left of the item graphic
|
||||
itemPosition.x += frame.width / 2; // Align position to center of the item graphic
|
||||
itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box
|
||||
|
||||
const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition); |
||||
|
||||
return { { screen.x, screen.y }, { maxW, totalH } }; |
||||
} |
||||
|
||||
// 2) Inventory grid (Rect position)
|
||||
if (pcursinvitem >= INVITEM_INV_FIRST && pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) { |
||||
const int itemIdx = pcursinvitem - INVITEM_INV_FIRST; |
||||
|
||||
for (int j = 0; j < InventoryGridCells; ++j) { |
||||
if (player.InvGrid[j] > 0 && player.InvGrid[j] - 1 == itemIdx) { |
||||
const Item &it = player.InvList[itemIdx]; |
||||
Point itemPosition = InvRect[j + SLOTXY_INV_FIRST].position; |
||||
|
||||
itemPosition.x += GetInventorySize(it).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic
|
||||
itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box
|
||||
|
||||
const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition); |
||||
|
||||
return { { screen.x, screen.y }, { maxW, totalH } }; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 3) Belt (Rect position)
|
||||
if (pcursinvitem >= INVITEM_BELT_FIRST && pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) { |
||||
const int itemIdx = pcursinvitem - INVITEM_BELT_FIRST; |
||||
for (int i = 0; i < MaxBeltItems; ++i) { |
||||
if (player.SpdList[i].isEmpty()) |
||||
continue; |
||||
if (i != itemIdx) |
||||
continue; |
||||
|
||||
const Item &item = player.SpdList[i]; |
||||
Point itemPosition = InvRect[i + SLOTXY_BELT_FIRST].position; |
||||
|
||||
itemPosition.x += GetInventorySize(item).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic
|
||||
itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box
|
||||
|
||||
const Point screen = GetMainPanel().position + Displacement { itemPosition.x, itemPosition.y }; |
||||
|
||||
return { { screen.x, screen.y }, { maxW, totalH } }; |
||||
} |
||||
} |
||||
|
||||
// 4) Stash (Rect position)
|
||||
if (pcursstashitem != StashStruct::EmptyCell) { |
||||
for (auto slot : StashGridRange) { |
||||
auto itemId = Stash.GetItemIdAtPosition(slot); |
||||
if (itemId == StashStruct::EmptyCell) |
||||
continue; |
||||
if (itemId != pcursstashitem) |
||||
continue; |
||||
|
||||
const Item &item = Stash.stashList[itemId]; |
||||
Point itemPosition = GetStashSlotCoord(slot); |
||||
const Size itemGridSize = GetInventorySize(item); |
||||
|
||||
itemPosition.y += itemGridSize.height * (InventorySlotSizeInPixels.height + 1) - 1; // Align position to bottom left of the item graphic
|
||||
itemPosition.x += itemGridSize.width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic
|
||||
itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box
|
||||
|
||||
return { { itemPosition.x, itemPosition.y }, { maxW, totalH } }; |
||||
} |
||||
} |
||||
|
||||
return { { 0, 0 }, { 0, 0 } }; |
||||
} |
||||
|
||||
int GetHoverSpriteHeight() |
||||
{ |
||||
if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) { |
||||
auto &it = (*InspectPlayer).InvBody[pcursinvitem - INVITEM_HEAD]; |
||||
return GetInvItemSize(it._iCurs + CURSOR_FIRSTITEM).height + 1; |
||||
} |
||||
if (pcursinvitem >= INVITEM_INV_FIRST |
||||
&& pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) { |
||||
const int idx = pcursinvitem - INVITEM_INV_FIRST; |
||||
auto &it = (*InspectPlayer).InvList[idx]; |
||||
return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1) |
||||
- InventorySlotSizeInPixels.height; |
||||
} |
||||
if (pcursinvitem >= INVITEM_BELT_FIRST |
||||
&& pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) { |
||||
const int idx = pcursinvitem - INVITEM_BELT_FIRST; |
||||
auto &it = (*InspectPlayer).SpdList[idx]; |
||||
return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1) |
||||
- InventorySlotSizeInPixels.height - 1; |
||||
} |
||||
if (pcursstashitem != StashStruct::EmptyCell) { |
||||
auto &it = Stash.stashList[pcursstashitem]; |
||||
return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1); |
||||
} |
||||
return InventorySlotSizeInPixels.height; |
||||
} |
||||
|
||||
int ClampAboveOrBelow(int anchorY, int spriteH, int boxH, int pad, int linePad) |
||||
{ |
||||
const int yAbove = anchorY - spriteH - boxH - pad; |
||||
const int yBelow = anchorY + linePad / 2 + pad; |
||||
return (yAbove >= 0) ? yAbove : yBelow; |
||||
} |
||||
|
||||
void PrintFloatingInfo(const Surface &out) |
||||
{ |
||||
if (ChatFlag) |
||||
return; |
||||
if (FloatingInfoString.empty()) |
||||
return; |
||||
|
||||
const int verticalSpacing = 3; |
||||
const int lineHeight = 12 + verticalSpacing; |
||||
const int textSpacing = 2; |
||||
const int hPadding = 5; |
||||
const int vPadding = 4; |
||||
|
||||
Rectangle floatingInfoBox = GetFloatingInfoRect(lineHeight, textSpacing); |
||||
|
||||
// Prevent the floating info box from going off-screen horizontally
|
||||
floatingInfoBox.position.x = std::clamp(floatingInfoBox.position.x, hPadding, GetScreenWidth() - (floatingInfoBox.size.width + hPadding)); |
||||
|
||||
const int spriteH = GetHoverSpriteHeight(); |
||||
const int anchorY = floatingInfoBox.position.y; |
||||
|
||||
// Prevent the floating info box from going off-screen vertically
|
||||
floatingInfoBox.position.y = ClampAboveOrBelow(anchorY, spriteH, floatingInfoBox.size.height, vPadding, verticalSpacing); |
||||
|
||||
SpeakText(FloatingInfoString); |
||||
|
||||
for (int i = 0; i < 3; i++) |
||||
DrawHalfTransparentRectTo(out, floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding, floatingInfoBox.size.width + hPadding * 2, floatingInfoBox.size.height + vPadding * 2); |
||||
DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x - hPadding - 1, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10); |
||||
DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x + hPadding + floatingInfoBox.size.width, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10); |
||||
DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10); |
||||
DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y + vPadding + floatingInfoBox.size.height }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10); |
||||
|
||||
DrawString(out, FloatingInfoString, floatingInfoBox, |
||||
{ |
||||
.flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter, |
||||
.spacing = textSpacing, |
||||
.lineHeight = lineHeight, |
||||
}); |
||||
} |
||||
|
||||
} // namespace
|
||||
|
||||
void AddInfoBoxString(std::string_view str, bool floatingBox /*= false*/) |
||||
{ |
||||
StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; |
||||
|
||||
if (infoString.empty()) |
||||
infoString = str; |
||||
else |
||||
infoString = StrCat(infoString, "\n", str); |
||||
} |
||||
|
||||
void AddInfoBoxString(std::string &&str, bool floatingBox /*= false*/) |
||||
{ |
||||
StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; |
||||
|
||||
if (infoString.empty()) |
||||
infoString = std::move(str); |
||||
else |
||||
infoString = StrCat(infoString, "\n", str); |
||||
} |
||||
|
||||
void CheckPanelInfo() |
||||
{ |
||||
MainPanelFlag = false; |
||||
InfoString = StringOrView {}; |
||||
FloatingInfoString = StringOrView {}; |
||||
|
||||
const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons; |
||||
|
||||
for (int i = 0; i < totalButtons; i++) { |
||||
Rectangle button = MainPanelButtonRect[i]; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, button); |
||||
|
||||
if (button.contains(MousePosition)) { |
||||
if (i != 7) { |
||||
InfoString = _(PanBtnStr[i]); |
||||
} else { |
||||
if (MyPlayer->friendlyMode) |
||||
InfoString = _("Player friendly"); |
||||
else |
||||
InfoString = _("Player attack"); |
||||
} |
||||
if (PanBtnHotKey[i] != nullptr) { |
||||
AddInfoBoxString(fmt::format(fmt::runtime(_("Hotkey: {:s}")), _(PanBtnHotKey[i]))); |
||||
} |
||||
InfoColor = UiFlags::ColorWhite; |
||||
MainPanelFlag = true; |
||||
} |
||||
} |
||||
|
||||
Rectangle spellSelectButton = SpellButtonRect; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, spellSelectButton); |
||||
|
||||
if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) { |
||||
InfoString = _("Select current spell button"); |
||||
InfoColor = UiFlags::ColorWhite; |
||||
MainPanelFlag = true; |
||||
AddInfoBoxString(_("Hotkey: 's'")); |
||||
const Player &myPlayer = *MyPlayer; |
||||
const SpellID spellId = myPlayer._pRSpell; |
||||
if (IsValidSpell(spellId)) { |
||||
switch (myPlayer._pRSplType) { |
||||
case SpellType::Skill: |
||||
AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Skill")), pgettext("spell", GetSpellData(spellId).sNameText))); |
||||
break; |
||||
case SpellType::Spell: { |
||||
AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Spell")), pgettext("spell", GetSpellData(spellId).sNameText))); |
||||
const int spellLevel = myPlayer.GetSpellLevel(spellId); |
||||
AddInfoBoxString(spellLevel == 0 ? _("Spell Level 0 - Unusable") : fmt::format(fmt::runtime(_("Spell Level {:d}")), spellLevel)); |
||||
} break; |
||||
case SpellType::Scroll: { |
||||
AddInfoBoxString(fmt::format(fmt::runtime(_("Scroll of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); |
||||
const int scrollCount = c_count_if(InventoryAndBeltPlayerItemsRange { myPlayer }, [spellId](const Item &item) { |
||||
return item.isScrollOf(spellId); |
||||
}); |
||||
AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount)), scrollCount)); |
||||
} break; |
||||
case SpellType::Charges: |
||||
AddInfoBoxString(fmt::format(fmt::runtime(_("Staff of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); |
||||
AddInfoBoxString(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; |
||||
} |
||||
} |
||||
} |
||||
|
||||
Rectangle belt = BeltRect; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, belt); |
||||
|
||||
if (belt.contains(MousePosition)) |
||||
pcursinvitem = CheckInvHLight(); |
||||
|
||||
if (CheckXPBarInfo()) |
||||
MainPanelFlag = true; |
||||
} |
||||
|
||||
void DrawInfoBox(const Surface &out) |
||||
{ |
||||
DrawPanelBox(out, MakeSdlRect(InfoBoxRect.position.x, InfoBoxRect.position.y + PanelPaddingHeight, InfoBoxRect.size.width, InfoBoxRect.size.height), GetMainPanel().position + Displacement { InfoBoxRect.position.x, InfoBoxRect.position.y }); |
||||
if (!MainPanelFlag && !trigflag && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && !SpellSelectFlag && pcurs != CURSOR_HOURGLASS) { |
||||
InfoString = StringOrView {}; |
||||
InfoColor = UiFlags::ColorWhite; |
||||
} |
||||
const Player &myPlayer = *MyPlayer; |
||||
if (SpellSelectFlag || trigflag || pcurs == CURSOR_HOURGLASS) { |
||||
InfoColor = UiFlags::ColorWhite; |
||||
} else if (!myPlayer.HoldItem.isEmpty()) { |
||||
if (myPlayer.HoldItem._itype == ItemType::Gold) { |
||||
const 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 Monster &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 = std::string_view(Towners[pcursmonst].name); |
||||
} |
||||
} |
||||
if (PlayerUnderCursor != nullptr) { |
||||
InfoColor = UiFlags::ColorWhitegold; |
||||
const auto &target = *PlayerUnderCursor; |
||||
InfoString = std::string_view(target._pName); |
||||
AddInfoBoxString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), target.getClassName(), target.getCharacterLevel())); |
||||
AddInfoBoxString(fmt::format(fmt::runtime(_("Hit Points {:d} of {:d}")), target._pHitPoints >> 6, target._pMaxHP >> 6)); |
||||
} |
||||
if (PortraitIdUnderCursor != -1) { |
||||
InfoColor = UiFlags::ColorWhitegold; |
||||
auto &target = Players[PortraitIdUnderCursor]; |
||||
InfoString = std::string_view(target._pName); |
||||
AddInfoBoxString(_("Right click to inspect")); |
||||
} |
||||
} |
||||
if (!InfoString.empty()) |
||||
PrintInfo(out); |
||||
} |
||||
|
||||
void DrawFloatingInfoBox(const Surface &out) |
||||
{ |
||||
if (pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell) { |
||||
FloatingInfoString = StringOrView {}; |
||||
InfoColor = UiFlags::ColorWhite; |
||||
} |
||||
|
||||
if (!FloatingInfoString.empty()) |
||||
PrintFloatingInfo(out); |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,832 @@
|
||||
#include "control_panel.hpp" |
||||
#include "control.hpp" |
||||
#include "control_chat.hpp" |
||||
#include "control_flasks.hpp" |
||||
|
||||
#include "automap.h" |
||||
#include "controls/control_mode.hpp" |
||||
#include "controls/modifier_hints.h" |
||||
#include "diablo_msg.hpp" |
||||
#include "engine/backbuffer_state.hpp" |
||||
#include "engine/load_cel.hpp" |
||||
#include "engine/render/clx_render.hpp" |
||||
#include "engine/trn.hpp" |
||||
#include "gamemenu.h" |
||||
#include "headless_mode.hpp" |
||||
#include "minitext.h" |
||||
#include "options.h" |
||||
#include "panels/charpanel.hpp" |
||||
#include "panels/mainpanel.hpp" |
||||
#include "panels/partypanel.hpp" |
||||
#include "panels/spell_book.hpp" |
||||
#include "panels/spell_icons.hpp" |
||||
#include "panels/spell_list.hpp" |
||||
#include "pfile.h" |
||||
#include "qol/stash.h" |
||||
#include "stores.h" |
||||
#include "utils/sdl_compat.h" |
||||
|
||||
namespace devilution { |
||||
|
||||
bool CharPanelButton[4]; |
||||
bool LevelButtonDown; |
||||
bool CharPanelButtonActive; |
||||
UiFlags InfoColor; |
||||
int SpellbookTab; |
||||
bool ChatFlag; |
||||
bool SpellbookFlag; |
||||
bool CharFlag; |
||||
bool MainPanelFlag; |
||||
bool MainPanelButtonDown; |
||||
bool SpellSelectFlag; |
||||
Rectangle MainPanel; |
||||
Rectangle LeftPanel; |
||||
Rectangle RightPanel; |
||||
std::optional<OwnedSurface> BottomBuffer; |
||||
OptionalOwnedClxSpriteList GoldBoxBuffer; |
||||
|
||||
const Rectangle &GetMainPanel() |
||||
{ |
||||
return MainPanel; |
||||
} |
||||
const Rectangle &GetLeftPanel() |
||||
{ |
||||
return LeftPanel; |
||||
} |
||||
const Rectangle &GetRightPanel() |
||||
{ |
||||
return RightPanel; |
||||
} |
||||
bool IsLeftPanelOpen() |
||||
{ |
||||
return CharFlag || QuestLogIsOpen || IsStashOpen; |
||||
} |
||||
bool IsRightPanelOpen() |
||||
{ |
||||
return invflag || SpellbookFlag; |
||||
} |
||||
|
||||
constexpr Size IncrementAttributeButtonSize { 41, 22 }; |
||||
/** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */ |
||||
Rectangle CharPanelButtonRect[4] = { |
||||
{ { 137, 138 }, IncrementAttributeButtonSize }, |
||||
{ { 137, 166 }, IncrementAttributeButtonSize }, |
||||
{ { 137, 195 }, IncrementAttributeButtonSize }, |
||||
{ { 137, 223 }, IncrementAttributeButtonSize } |
||||
}; |
||||
|
||||
constexpr Size WidePanelButtonSize { 71, 20 }; |
||||
constexpr Size PanelButtonSize { 33, 32 }; |
||||
/** Positions of panel buttons. */ |
||||
Rectangle MainPanelButtonRect[8] = { |
||||
// clang-format off
|
||||
{ { 9, 9 }, WidePanelButtonSize }, // char button
|
||||
{ { 9, 35 }, WidePanelButtonSize }, // quests button
|
||||
{ { 9, 75 }, WidePanelButtonSize }, // map button
|
||||
{ { 9, 101 }, WidePanelButtonSize }, // menu button
|
||||
{ { 560, 9 }, WidePanelButtonSize }, // inv button
|
||||
{ { 560, 35 }, WidePanelButtonSize }, // spells button
|
||||
{ { 87, 91 }, PanelButtonSize }, // chat button
|
||||
{ { 527, 91 }, PanelButtonSize }, // friendly fire button
|
||||
// clang-format on
|
||||
}; |
||||
|
||||
Rectangle LevelButtonRect = { { 40, -39 }, { 41, 22 } }; |
||||
|
||||
constexpr int BeltItems = 8; |
||||
constexpr Size BeltSize { (INV_SLOT_SIZE_PX + 1) * BeltItems, INV_SLOT_SIZE_PX }; |
||||
Rectangle BeltRect { { 205, 5 }, BeltSize }; |
||||
|
||||
Rectangle SpellButtonRect { { 565, 64 }, { 56, 56 } }; |
||||
|
||||
int PanelPaddingHeight = 16; |
||||
|
||||
/** 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 panel_button_id to hotkey name. */ |
||||
const char *const PanBtnHotKey[8] = { "'c'", "'q'", N_("Tab"), N_("Esc"), "'i'", "'b'", N_("Enter"), nullptr }; |
||||
|
||||
int TotalSpMainPanelButtons = 6; |
||||
int TotalMpMainPanelButtons = 8; |
||||
|
||||
namespace { |
||||
|
||||
OptionalOwnedClxSpriteList pDurIcons; |
||||
OptionalOwnedClxSpriteList multiButtons; |
||||
OptionalOwnedClxSpriteList pMainPanelButtons; |
||||
|
||||
enum panel_button_id : uint8_t { |
||||
PanelButtonCharinfo, |
||||
PanelButtonFirst = PanelButtonCharinfo, |
||||
PanelButtonQlog, |
||||
PanelButtonAutomap, |
||||
PanelButtonMainmenu, |
||||
PanelButtonInventory, |
||||
PanelButtonSpellbook, |
||||
PanelButtonSendmsg, |
||||
PanelButtonFriendly, |
||||
PanelButtonLast = PanelButtonFriendly, |
||||
}; |
||||
|
||||
bool MainPanelButtons[PanelButtonLast + 1]; |
||||
|
||||
void SetMainPanelButtonDown(int btnId) |
||||
{ |
||||
MainPanelButtons[btnId] = true; |
||||
RedrawComponent(PanelDrawComponent::ControlButtons); |
||||
MainPanelButtonDown = true; |
||||
} |
||||
|
||||
void SetMainPanelButtonUp() |
||||
{ |
||||
RedrawComponent(PanelDrawComponent::ControlButtons); |
||||
MainPanelButtonDown = false; |
||||
} |
||||
|
||||
int CapStatPointsToAdd(int remainingStatPoints, const Player &player, CharacterAttribute attribute) |
||||
{ |
||||
const int pointsToReachCap = player.GetMaximumAttributeValue(attribute) - player.GetBaseAttributeValue(attribute); |
||||
|
||||
return std::min(remainingStatPoints, pointsToReachCap); |
||||
} |
||||
|
||||
int DrawDurIcon4Item(const Surface &out, Item &pItem, int x, int c) |
||||
{ |
||||
const int durabilityThresholdGold = 5; |
||||
const int durabilityThresholdRed = 2; |
||||
|
||||
if (pItem.isEmpty()) |
||||
return x; |
||||
if (pItem._iDurability > durabilityThresholdGold) |
||||
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; |
||||
} |
||||
} |
||||
|
||||
// Calculate how much of the icon should be gold and red
|
||||
const int height = (*pDurIcons)[c].height(); // Height of durability icon CEL
|
||||
int partition = 0; |
||||
if (pItem._iDurability > durabilityThresholdRed) { |
||||
const int current = pItem._iDurability - durabilityThresholdRed; |
||||
partition = (height * current) / (durabilityThresholdGold - durabilityThresholdRed); |
||||
} |
||||
|
||||
// Draw icon
|
||||
const int y = -17 + GetMainPanel().position.y; |
||||
if (partition > 0) { |
||||
const Surface stenciledBuffer = out.subregionY(y - partition, partition); |
||||
ClxDraw(stenciledBuffer, { x, partition }, (*pDurIcons)[c + 8]); // Gold icon
|
||||
} |
||||
if (partition != height) { |
||||
const Surface stenciledBuffer = out.subregionY(y - height, height - partition); |
||||
ClxDraw(stenciledBuffer, { x, height }, (*pDurIcons)[c]); // Red icon
|
||||
} |
||||
|
||||
return x - (*pDurIcons)[c].height() - 8; // Add in spacing for the next durability icon
|
||||
} |
||||
|
||||
bool IsLevelUpButtonVisible() |
||||
{ |
||||
if (SpellSelectFlag || CharFlag || MyPlayer->_pStatPts == 0) { |
||||
return false; |
||||
} |
||||
if (ControlMode == ControlTypes::VirtualGamepad) { |
||||
return false; |
||||
} |
||||
if (IsPlayerInStore() || 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; |
||||
} |
||||
} |
||||
|
||||
void FocusOnCharInfo() |
||||
{ |
||||
const 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(CharPanelButtonRect[stat].Center()); |
||||
} |
||||
|
||||
void OpenCharPanel() |
||||
{ |
||||
QuestLogIsOpen = false; |
||||
CloseGoldWithdraw(); |
||||
CloseStash(); |
||||
CharFlag = true; |
||||
} |
||||
|
||||
void CloseCharPanel() |
||||
{ |
||||
CharFlag = false; |
||||
if (IsInspectingPlayer()) { |
||||
InspectPlayer = MyPlayer; |
||||
RedrawEverything(); |
||||
|
||||
if (InspectingFromPartyPanel) |
||||
InspectingFromPartyPanel = false; |
||||
else |
||||
InitDiabloMsg(_("Stopped inspecting players.")); |
||||
} |
||||
} |
||||
|
||||
void ToggleCharPanel() |
||||
{ |
||||
if (CharFlag) |
||||
CloseCharPanel(); |
||||
else |
||||
OpenCharPanel(); |
||||
} |
||||
|
||||
Point GetPanelPosition(UiPanels panel, Point offset) |
||||
{ |
||||
const 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(*BottomBuffer, srcRect, targetPosition); |
||||
} |
||||
|
||||
tl::expected<void, std::string> InitMainPanel() |
||||
{ |
||||
if (!HeadlessMode) { |
||||
BottomBuffer.emplace(GetMainPanel().size.width, (GetMainPanel().size.height + PanelPaddingHeight) * (IsChatAvailable() ? 2 : 1)); |
||||
pManaBuff.emplace(88, 88); |
||||
pLifeBuff.emplace(88, 88); |
||||
|
||||
RETURN_IF_ERROR(LoadPartyPanel()); |
||||
RETURN_IF_ERROR(LoadCharPanel()); |
||||
RETURN_IF_ERROR(LoadLargeSpellIcons()); |
||||
{ |
||||
ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\panel8", GetMainPanel().size.width)); |
||||
ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) - 1 }, sprite[0]); |
||||
} |
||||
{ |
||||
const Point bulbsPosition { 0, 87 }; |
||||
ASSIGN_OR_RETURN(const OwnedClxSpriteList statusPanel, LoadCelWithStatus("ctrlpan\\p8bulbs", 88)); |
||||
ClxDraw(*pLifeBuff, bulbsPosition, statusPanel[0]); |
||||
ClxDraw(*pManaBuff, bulbsPosition, statusPanel[1]); |
||||
} |
||||
} |
||||
ChatFlag = false; |
||||
ChatInputState = std::nullopt; |
||||
if (IsChatAvailable()) { |
||||
if (!HeadlessMode) { |
||||
{ |
||||
ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\talkpanl", GetMainPanel().size.width)); |
||||
ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) * 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; |
||||
} |
||||
MainPanelFlag = false; |
||||
LevelButtonDown = false; |
||||
if (!HeadlessMode) { |
||||
RETURN_IF_ERROR(LoadMainPanel()); |
||||
ASSIGN_OR_RETURN(pMainPanelButtons, LoadCelWithStatus("ctrlpan\\panel8bu", 71)); |
||||
|
||||
static const uint16_t CharButtonsFrameWidths[9] { 95, 41, 41, 41, 41, 41, 41, 41, 41 }; |
||||
ASSIGN_OR_RETURN(pChrButtons, LoadCelWithStatus("data\\charbut", CharButtonsFrameWidths)); |
||||
} |
||||
ResetMainPanelButtons(); |
||||
if (!HeadlessMode) |
||||
pDurIcons = LoadCel("items\\duricons", 32); |
||||
for (bool &buttonEnabled : CharPanelButton) |
||||
buttonEnabled = false; |
||||
CharPanelButtonActive = false; |
||||
InfoString = StringOrView {}; |
||||
FloatingInfoString = StringOrView {}; |
||||
RedrawComponent(PanelDrawComponent::Health); |
||||
RedrawComponent(PanelDrawComponent::Mana); |
||||
CloseCharPanel(); |
||||
SpellSelectFlag = false; |
||||
SpellbookTab = 0; |
||||
SpellbookFlag = false; |
||||
|
||||
if (!HeadlessMode) { |
||||
InitSpellBook(); |
||||
ASSIGN_OR_RETURN(pQLogCel, LoadCelWithStatus("data\\quest", static_cast<uint16_t>(SidePanelSize.width))); |
||||
ASSIGN_OR_RETURN(GoldBoxBuffer, LoadCelWithStatus("ctrlpan\\golddrop", 261)); |
||||
} |
||||
CloseGoldDrop(); |
||||
CalculatePanelAreas(); |
||||
|
||||
if (!HeadlessMode) |
||||
InitModifierHints(); |
||||
|
||||
return {}; |
||||
} |
||||
|
||||
void DrawMainPanel(const Surface &out) |
||||
{ |
||||
DrawPanelBox(out, MakeSdlRect(0, sgbPlrTalkTbl + PanelPaddingHeight, GetMainPanel().size.width, GetMainPanel().size.height), GetMainPanel().position); |
||||
DrawInfoBox(out); |
||||
} |
||||
|
||||
void DrawMainPanelButtons(const Surface &out) |
||||
{ |
||||
const Point mainPanelPosition = GetMainPanel().position; |
||||
|
||||
for (int i = 0; i < TotalSpMainPanelButtons; i++) { |
||||
if (!MainPanelButtons[i]) { |
||||
DrawPanelBox(out, MakeSdlRect(MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y + PanelPaddingHeight, MainPanelButtonRect[i].size.width, MainPanelButtonRect[i].size.height + 1), mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y }); |
||||
} else { |
||||
const Point position = mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y }; |
||||
RenderClxSprite(out, (*pMainPanelButtons)[i], position); |
||||
RenderClxSprite(out, (*PanelButtonDown)[i], position + Displacement { 4, 0 }); |
||||
} |
||||
} |
||||
|
||||
if (IsChatAvailable()) { |
||||
RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonSendmsg] ? 1 : 0], mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonSendmsg].position.x, MainPanelButtonRect[PanelButtonSendmsg].position.y }); |
||||
|
||||
const Point friendlyButtonPosition = mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonFriendly].position.x, MainPanelButtonRect[PanelButtonFriendly].position.y }; |
||||
|
||||
if (MyPlayer->friendlyMode) |
||||
RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 3 : 2], friendlyButtonPosition); |
||||
else |
||||
RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 5 : 4], friendlyButtonPosition); |
||||
} |
||||
} |
||||
|
||||
void ResetMainPanelButtons() |
||||
{ |
||||
for (bool &panelButton : MainPanelButtons) |
||||
panelButton = false; |
||||
SetMainPanelButtonUp(); |
||||
} |
||||
|
||||
void CheckMainPanelButton() |
||||
{ |
||||
const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons; |
||||
|
||||
for (int i = 0; i < totalButtons; i++) { |
||||
Rectangle button = MainPanelButtonRect[i]; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, button); |
||||
|
||||
if (button.contains(MousePosition)) { |
||||
SetMainPanelButtonDown(i); |
||||
} |
||||
} |
||||
|
||||
Rectangle spellSelectButton = SpellButtonRect; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, spellSelectButton); |
||||
|
||||
if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) { |
||||
if ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { |
||||
Player &myPlayer = *MyPlayer; |
||||
myPlayer._pRSpell = SpellID::Invalid; |
||||
myPlayer._pRSplType = SpellType::Invalid; |
||||
RedrawEverything(); |
||||
return; |
||||
} |
||||
DoSpeedBook(); |
||||
gamemenu_off(); |
||||
} |
||||
} |
||||
|
||||
void CheckMainPanelButtonDead() |
||||
{ |
||||
Rectangle menuButton = MainPanelButtonRect[PanelButtonMainmenu]; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, menuButton); |
||||
|
||||
if (menuButton.contains(MousePosition)) { |
||||
SetMainPanelButtonDown(PanelButtonMainmenu); |
||||
return; |
||||
} |
||||
|
||||
Rectangle chatButton = MainPanelButtonRect[PanelButtonSendmsg]; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, chatButton); |
||||
|
||||
if (chatButton.contains(MousePosition)) { |
||||
SetMainPanelButtonDown(PanelButtonSendmsg); |
||||
} |
||||
} |
||||
|
||||
void DoAutoMap() |
||||
{ |
||||
if (!AutomapActive) |
||||
StartAutomap(); |
||||
else |
||||
AutomapActive = false; |
||||
} |
||||
|
||||
void CycleAutomapType() |
||||
{ |
||||
if (!AutomapActive) { |
||||
StartAutomap(); |
||||
return; |
||||
} |
||||
const AutomapType newType { static_cast<std::underlying_type_t<AutomapType>>( |
||||
(static_cast<unsigned>(GetAutomapType()) + 1) % enum_size<AutomapType>::value) }; |
||||
SetAutomapType(newType); |
||||
if (newType == AutomapType::FIRST) { |
||||
AutomapActive = false; |
||||
} |
||||
} |
||||
|
||||
void CheckMainPanelButtonUp() |
||||
{ |
||||
bool gamemenuOff = true; |
||||
|
||||
SetMainPanelButtonUp(); |
||||
|
||||
for (int i = PanelButtonFirst; i <= PanelButtonLast; i++) { |
||||
if (!MainPanelButtons[i]) |
||||
continue; |
||||
|
||||
MainPanelButtons[i] = false; |
||||
|
||||
Rectangle button = MainPanelButtonRect[i]; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, button); |
||||
|
||||
if (!button.contains(MousePosition)) |
||||
continue; |
||||
|
||||
switch (i) { |
||||
case PanelButtonCharinfo: |
||||
ToggleCharPanel(); |
||||
break; |
||||
case PanelButtonQlog: |
||||
CloseCharPanel(); |
||||
CloseGoldWithdraw(); |
||||
CloseStash(); |
||||
if (!QuestLogIsOpen) |
||||
StartQuestlog(); |
||||
else |
||||
QuestLogIsOpen = false; |
||||
break; |
||||
case PanelButtonAutomap: |
||||
DoAutoMap(); |
||||
break; |
||||
case PanelButtonMainmenu: |
||||
if (MyPlayerIsDead) { |
||||
if (!gbIsMultiplayer) { |
||||
if (gbValidSaveFile) |
||||
gamemenu_load_game(false); |
||||
else |
||||
gamemenu_exit_game(false); |
||||
} else { |
||||
NetSendCmd(true, CMD_RETOWN); |
||||
} |
||||
break; |
||||
} else if (MyPlayer->hasNoLife()) { |
||||
break; |
||||
} |
||||
qtextflag = false; |
||||
gamemenu_handle_previous(); |
||||
gamemenuOff = false; |
||||
break; |
||||
case PanelButtonInventory: |
||||
SpellbookFlag = false; |
||||
CloseGoldWithdraw(); |
||||
CloseStash(); |
||||
invflag = !invflag; |
||||
CloseGoldDrop(); |
||||
break; |
||||
case PanelButtonSpellbook: |
||||
CloseInventory(); |
||||
CloseGoldDrop(); |
||||
SpellbookFlag = !SpellbookFlag; |
||||
break; |
||||
case PanelButtonSendmsg: |
||||
if (ChatFlag) |
||||
ResetChat(); |
||||
else |
||||
TypeChatMessage(); |
||||
break; |
||||
case PanelButtonFriendly: |
||||
// Toggle friendly Mode
|
||||
NetSendCmd(true, CMD_FRIENDLYMODE); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (gamemenuOff) |
||||
gamemenu_off(); |
||||
} |
||||
|
||||
void FreeControlPan() |
||||
{ |
||||
BottomBuffer = std::nullopt; |
||||
pManaBuff = std::nullopt; |
||||
pLifeBuff = std::nullopt; |
||||
FreeLargeSpellIcons(); |
||||
FreeSpellBook(); |
||||
pMainPanelButtons = std::nullopt; |
||||
multiButtons = std::nullopt; |
||||
talkButtons = std::nullopt; |
||||
pChrButtons = std::nullopt; |
||||
pDurIcons = std::nullopt; |
||||
pQLogCel = std::nullopt; |
||||
GoldBoxBuffer = std::nullopt; |
||||
FreeMainPanel(); |
||||
FreePartyPanel(); |
||||
FreeCharPanel(); |
||||
FreeModifierHints(); |
||||
} |
||||
|
||||
void CheckLevelButton() |
||||
{ |
||||
if (!IsLevelUpButtonVisible()) { |
||||
return; |
||||
} |
||||
|
||||
Rectangle button = LevelButtonRect; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, button); |
||||
|
||||
if (!LevelButtonDown && button.contains(MousePosition)) |
||||
LevelButtonDown = true; |
||||
} |
||||
|
||||
void CheckLevelButtonUp() |
||||
{ |
||||
Rectangle button = LevelButtonRect; |
||||
|
||||
SetPanelObjectPosition(UiPanels::Main, button); |
||||
|
||||
if (button.contains(MousePosition)) { |
||||
OpenCharPanel(); |
||||
} |
||||
LevelButtonDown = false; |
||||
} |
||||
|
||||
void DrawLevelButton(const Surface &out) |
||||
{ |
||||
if (IsLevelUpButtonVisible()) { |
||||
const int nCel = LevelButtonDown ? 2 : 1; |
||||
DrawString(out, _("Level Up"), { GetMainPanel().position + Displacement { 0, LevelButtonRect.position.y - 23 }, { 120, 0 } }, |
||||
{ .flags = UiFlags::ColorWhite | UiFlags::AlignCenter | UiFlags::KerningFitSpacing }); |
||||
RenderClxSprite(out, (*pChrButtons)[nCel], GetMainPanel().position + Displacement { LevelButtonRect.position.x, LevelButtonRect.position.y }); |
||||
} |
||||
} |
||||
|
||||
void CheckChrBtns() |
||||
{ |
||||
const Player &myPlayer = *MyPlayer; |
||||
|
||||
if (myPlayer._pmode == PM_DEATH) |
||||
return; |
||||
|
||||
if (CharPanelButtonActive || 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 = CharPanelButtonRect[buttonId]; |
||||
SetPanelObjectPosition(UiPanels::Character, button); |
||||
if (button.contains(MousePosition)) { |
||||
CharPanelButton[buttonId] = true; |
||||
CharPanelButtonActive = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
void ReleaseChrBtns(bool addAllStatPoints) |
||||
{ |
||||
const Player &myPlayer = *MyPlayer; |
||||
|
||||
if (myPlayer._pmode == PM_DEATH) |
||||
return; |
||||
|
||||
CharPanelButtonActive = false; |
||||
for (auto attribute : enum_values<CharacterAttribute>()) { |
||||
auto buttonId = static_cast<size_t>(attribute); |
||||
if (!CharPanelButton[buttonId]) |
||||
continue; |
||||
|
||||
CharPanelButton[buttonId] = false; |
||||
Rectangle button = CharPanelButtonRect[buttonId]; |
||||
SetPanelObjectPosition(UiPanels::Character, button); |
||||
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) |
||||
{ |
||||
const bool hasRoomBetweenPanels = RightPanel.position.x - (LeftPanel.position.x + LeftPanel.size.width) >= 16 + (32 + 8 + 32 + 8 + 32 + 8 + 32) + 16; |
||||
const 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 DrawDeathText(const Surface &out) |
||||
{ |
||||
const TextRenderOptions largeTextOptions { |
||||
.flags = UiFlags::FontSize42 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, |
||||
.spacing = 2 |
||||
}; |
||||
const TextRenderOptions smallTextOptions { |
||||
.flags = UiFlags::FontSize30 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, |
||||
.spacing = 2 |
||||
}; |
||||
std::string text; |
||||
const int verticalPadding = 42; |
||||
Point linePosition { 0, gnScreenHeight / 2 - (verticalPadding * 2) }; |
||||
|
||||
text = _("You have died"); |
||||
DrawString(out, text, linePosition, largeTextOptions); |
||||
linePosition.y += verticalPadding; |
||||
|
||||
std::string buttonText; |
||||
|
||||
switch (ControlMode) { |
||||
case ControlTypes::KeyboardAndMouse: |
||||
buttonText = _("ESC"); |
||||
break; |
||||
case ControlTypes::Gamepad: |
||||
buttonText = ToString(GamepadType, ControllerButton_BUTTON_START); |
||||
break; |
||||
case ControlTypes::VirtualGamepad: |
||||
buttonText = _("Menu Button"); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
|
||||
if (!gbIsMultiplayer) { |
||||
if (gbValidSaveFile) |
||||
text = fmt::format(fmt::runtime(_("Press {} to load last save.")), buttonText); |
||||
else |
||||
text = fmt::format(fmt::runtime(_("Press {} to return to Main Menu.")), buttonText); |
||||
|
||||
} else { |
||||
text = fmt::format(fmt::runtime(_("Press {} to restart in town.")), buttonText); |
||||
} |
||||
DrawString(out, text, linePosition, smallTextOptions); |
||||
} |
||||
|
||||
void SetPanelObjectPosition(UiPanels panel, Rectangle &button) |
||||
{ |
||||
button.position = GetPanelPosition(panel, button.position); |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,18 @@
|
||||
#pragma once |
||||
|
||||
#include "engine/rectangle.hpp" |
||||
#include "panels/ui_panels.hpp" |
||||
|
||||
namespace devilution { |
||||
|
||||
extern int TotalSpMainPanelButtons; |
||||
extern int TotalMpMainPanelButtons; |
||||
extern int PanelPaddingHeight; |
||||
extern const char *const PanBtnStr[8]; |
||||
extern const char *const PanBtnHotKey[8]; |
||||
extern Rectangle SpellButtonRect; |
||||
extern Rectangle BeltRect; |
||||
|
||||
void SetPanelObjectPosition(UiPanels panel, Rectangle &button); |
||||
|
||||
} // namespace devilution
|
||||
@ -1,405 +1,405 @@
|
||||
/**
|
||||
* @file random.hpp |
||||
* |
||||
* Contains convenience functions for random number generation |
||||
* |
||||
* This includes specific engine/distribution functions for logic that needs to be compatible with the base game. |
||||
*/ |
||||
#pragma once |
||||
|
||||
#include <algorithm> |
||||
#include <cstdint> |
||||
#include <initializer_list> |
||||
#include <random> |
||||
|
||||
namespace devilution { |
||||
|
||||
class DiabloGenerator { |
||||
private: |
||||
/** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */ |
||||
std::linear_congruential_engine<uint32_t, 0x015A4E35, 1, 0> lcg; |
||||
|
||||
public: |
||||
/**
|
||||
* @brief Set the state of the RandomNumberEngine used by the base game to the specific seed |
||||
* @param seed New engine state |
||||
*/ |
||||
DiabloGenerator(uint32_t seed) |
||||
{ |
||||
lcg.seed(seed); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Advance the global RandomNumberEngine state by the specified number of rounds |
||||
* |
||||
* Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated. |
||||
* @param count How many values to discard |
||||
*/ |
||||
void discardRandomValues(unsigned count) |
||||
{ |
||||
lcg.discard(count); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random non-negative integer (most of the time) using the vanilla RNG |
||||
* |
||||
* This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try |
||||
* discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31. |
||||
* |
||||
* This function is only used when the base game wants to store the seed used to generate an item or level, however |
||||
* as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more |
||||
* appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game. |
||||
* |
||||
* @return A random number in the range [0,2^31) or -2^31 |
||||
*/ |
||||
int32_t advanceRndSeed() |
||||
{ |
||||
const int32_t seed = static_cast<int32_t>(lcg()); |
||||
// since abs(INT_MIN) is undefined behavior, handle this value specially
|
||||
return seed == std::numeric_limits<int32_t>::min() ? std::numeric_limits<int32_t>::min() : std::abs(seed); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random integer less than the given limit using the vanilla RNG |
||||
* |
||||
* If v is not a positive number this function returns 0 without calling the RNG. |
||||
* |
||||
* Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value |
||||
* less than 32768 for limits in that range. |
||||
* |
||||
* This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed() |
||||
* |
||||
* @see AdvanceRndSeed() |
||||
* @param v The upper limit for the return value |
||||
* @return A random number in the range [0, v) or rarely a negative value in (-v, -1] |
||||
*/ |
||||
int32_t generateRnd(int32_t v) |
||||
{ |
||||
if (v <= 0) |
||||
return 0; |
||||
if (v <= 0x7FFF) // use the high bits to correct for LCG bias
|
||||
return (advanceRndSeed() >> 16) % v; |
||||
return advanceRndSeed() % v; |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random boolean value using the vanilla RNG |
||||
* |
||||
* This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2 |
||||
* represents a 50/50 chance. |
||||
* |
||||
* @param frequency odds of returning a true value |
||||
* @return A random boolean value |
||||
*/ |
||||
bool flipCoin(unsigned frequency) |
||||
{ |
||||
// Casting here because GenerateRnd takes a signed argument when it should take and yield unsigned.
|
||||
return generateRnd(static_cast<int32_t>(frequency)) == 0; |
||||
} |
||||
|
||||
/**
|
||||
* @brief Picks one of the elements in the list randomly. |
||||
* |
||||
* @param values The values to pick from |
||||
* @return A random value from the 'values' list. |
||||
*/ |
||||
template <typename T> |
||||
const T pickRandomlyAmong(const std::initializer_list<T> &values) |
||||
{ |
||||
const auto index { std::max<int32_t>(generateRnd(static_cast<int32_t>(values.size())), 0) }; |
||||
|
||||
return *(values.begin() + index); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random non-negative integer |
||||
* |
||||
* Effectively the same as GenerateRnd but will never return a negative value |
||||
* @param v upper limit for the return value |
||||
* @return a value between 0 and v-1 inclusive, i.e. the range [0, v) |
||||
*/ |
||||
inline int32_t randomIntLessThan(int32_t v) |
||||
{ |
||||
return std::max<int32_t>(generateRnd(v), 0); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Randomly chooses a value somewhere within the given range |
||||
* @param min lower limit, minimum possible value |
||||
* @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range |
||||
* @param halfOpen whether to use the limits as a half-open range or not |
||||
* @return a randomly selected integer |
||||
*/ |
||||
inline int32_t randomIntBetween(int32_t min, int32_t max, bool halfOpen = false) |
||||
{ |
||||
return randomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min; |
||||
} |
||||
}; |
||||
|
||||
// Based on fmix32 implementation from MurmurHash3 created by Austin Appleby in 2008
|
||||
// https://github.com/aappleby/smhasher/blob/61a0530f28277f2e850bfc39600ce61d02b518de/src/MurmurHash3.cpp#L68
|
||||
// and adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
|
||||
//
|
||||
// See also:
|
||||
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
|
||||
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
|
||||
// https://doi.org/10.1145/2714064.2660195
|
||||
class SplitMix32 { |
||||
uint32_t state; |
||||
|
||||
public: |
||||
SplitMix32(uint32_t state) |
||||
: state(state) |
||||
{ |
||||
} |
||||
|
||||
uint32_t next() |
||||
{ |
||||
uint32_t z = (state += 0x9e3779b9); |
||||
z = (z ^ (z >> 16)) * 0x85ebca6b; |
||||
z = (z ^ (z >> 13)) * 0xc2b2ae35; |
||||
return z ^ (z >> 16); |
||||
} |
||||
|
||||
void generate(uint32_t *begin, const uint32_t *end) |
||||
{ |
||||
while (begin != end) { |
||||
*begin = next(); |
||||
++begin; |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// Adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
|
||||
//
|
||||
// See also:
|
||||
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
|
||||
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
|
||||
// https://doi.org/10.1145/2714064.2660195
|
||||
class SplitMix64 { |
||||
uint64_t state; |
||||
|
||||
public: |
||||
SplitMix64(uint64_t state) |
||||
: state(state) |
||||
{ |
||||
} |
||||
|
||||
uint64_t next() |
||||
{ |
||||
uint64_t z = (state += 0x9e3779b97f4a7c15); |
||||
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; |
||||
z = (z ^ (z >> 27)) * 0x94d049bb133111eb; |
||||
return z ^ (z >> 31); |
||||
} |
||||
|
||||
void generate(uint64_t *begin, const uint64_t *end) |
||||
{ |
||||
while (begin != end) { |
||||
*begin = next(); |
||||
++begin; |
||||
} |
||||
} |
||||
}; |
||||
|
||||
/** Adapted from https://prng.di.unimi.it/xoshiro128plusplus.c written in 2019 by David Blackman and Sebastiano Vigna */ |
||||
class xoshiro128plusplus { |
||||
public: |
||||
typedef uint32_t state[4]; |
||||
|
||||
xoshiro128plusplus() { seed(); } |
||||
xoshiro128plusplus(const state &s) { copy(this->s, s); } |
||||
xoshiro128plusplus(uint64_t initialSeed) { seed(initialSeed); } |
||||
xoshiro128plusplus(uint32_t initialSeed) { seed(initialSeed); } |
||||
|
||||
uint32_t next(); |
||||
|
||||
/* This is the jump function for the generator. It is equivalent
|
||||
to 2^64 calls to next(); it can be used to generate 2^64 |
||||
non-overlapping subsequences for parallel computations. */ |
||||
void jump() |
||||
{ |
||||
static constexpr uint32_t JUMP[] = { 0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b }; |
||||
|
||||
uint32_t s0 = 0; |
||||
uint32_t s1 = 0; |
||||
uint32_t s2 = 0; |
||||
uint32_t s3 = 0; |
||||
for (const uint32_t entry : JUMP) |
||||
for (int b = 0; b < 32; b++) { |
||||
if (entry & UINT32_C(1) << b) { |
||||
s0 ^= s[0]; |
||||
s1 ^= s[1]; |
||||
s2 ^= s[2]; |
||||
s3 ^= s[3]; |
||||
} |
||||
next(); |
||||
} |
||||
|
||||
s[0] = s0; |
||||
s[1] = s1; |
||||
s[2] = s2; |
||||
s[3] = s3; |
||||
} |
||||
|
||||
void save(state &s) const |
||||
{ |
||||
copy(s, this->s); |
||||
} |
||||
|
||||
private: |
||||
state s; |
||||
|
||||
void seed(uint64_t value) |
||||
{ |
||||
uint64_t seeds[2]; |
||||
SplitMix64 seedSequence { value }; |
||||
seedSequence.generate(seeds, seeds + 2); |
||||
|
||||
s[0] = static_cast<uint32_t>(seeds[0] >> 32); |
||||
s[1] = static_cast<uint32_t>(seeds[0]); |
||||
s[2] = static_cast<uint32_t>(seeds[1] >> 32); |
||||
s[3] = static_cast<uint32_t>(seeds[1]); |
||||
} |
||||
|
||||
void seed(uint32_t value) |
||||
{ |
||||
SplitMix32 seedSequence { value }; |
||||
seedSequence.generate(s, s + 4); |
||||
} |
||||
|
||||
void seed() |
||||
{ |
||||
seed(timeSeed()); |
||||
|
||||
#if !(defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0)) |
||||
static std::random_device rd; |
||||
std::uniform_int_distribution<uint32_t> dist; |
||||
for (uint32_t &cell : s) |
||||
cell ^= dist(rd); |
||||
#endif |
||||
} |
||||
|
||||
static uint64_t timeSeed(); |
||||
static void copy(state &dst, const state &src); |
||||
}; |
||||
|
||||
/**
|
||||
* @brief Returns a copy of the global seed generator and fast-forwards the global seed generator to avoid collisions |
||||
*/ |
||||
xoshiro128plusplus ReserveSeedSequence(); |
||||
|
||||
/**
|
||||
* @brief Advances the global seed generator state and returns the new value |
||||
*/ |
||||
uint32_t GenerateSeed(); |
||||
|
||||
/**
|
||||
* @brief Set the state of the RandomNumberEngine used by the base game to the specific seed |
||||
* @param seed New engine state |
||||
*/ |
||||
void SetRndSeed(uint32_t seed); |
||||
|
||||
/**
|
||||
* @brief Returns the current state of the RandomNumberEngine used by the base game |
||||
* |
||||
* This is only exposed to allow for debugging vanilla code and testing. Using this engine for new code is discouraged |
||||
* due to the poor randomness and bugs in the implementation that need to be retained for compatibility. |
||||
* |
||||
* @return The current engine state |
||||
*/ |
||||
uint32_t GetLCGEngineState(); |
||||
|
||||
/**
|
||||
* @brief Advance the global RandomNumberEngine state by the specified number of rounds |
||||
* |
||||
* Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated. |
||||
* @param count How many values to discard |
||||
*/ |
||||
void DiscardRandomValues(unsigned count); |
||||
|
||||
/**
|
||||
* @brief Advances the global RandomNumberEngine state and returns the new value |
||||
*/ |
||||
uint32_t GenerateRandomNumber(); |
||||
|
||||
/**
|
||||
* @brief Generates a random non-negative integer (most of the time) using the vanilla RNG |
||||
* |
||||
* This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try |
||||
* discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31. |
||||
* |
||||
* This function is only used when the base game wants to store the seed used to generate an item or level, however |
||||
* as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more |
||||
* appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game. |
||||
* |
||||
* @return A random number in the range [0,2^31) or -2^31 |
||||
*/ |
||||
[[nodiscard]] int32_t AdvanceRndSeed(); |
||||
|
||||
/**
|
||||
* @brief Generates a random integer less than the given limit using the vanilla RNG |
||||
* |
||||
* If v is not a positive number this function returns 0 without calling the RNG. |
||||
* |
||||
* Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value |
||||
* less than 32768 for limits in that range. |
||||
* |
||||
* This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed() |
||||
* |
||||
* @see AdvanceRndSeed() |
||||
* @param v The upper limit for the return value |
||||
* @return A random number in the range [0, v) or rarely a negative value in (-v, -1] |
||||
*/ |
||||
int32_t GenerateRnd(int32_t v); |
||||
|
||||
/**
|
||||
* @brief Generates a random boolean value using the vanilla RNG |
||||
* |
||||
* This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2 |
||||
* represents a 50/50 chance. |
||||
* |
||||
* @param frequency odds of returning a true value |
||||
* @return A random boolean value |
||||
*/ |
||||
bool FlipCoin(unsigned frequency = 2); |
||||
|
||||
/**
|
||||
* @brief Picks one of the elements in the list randomly. |
||||
* |
||||
* @param values The values to pick from |
||||
* @return A random value from the 'values' list. |
||||
*/ |
||||
template <typename T> |
||||
const T PickRandomlyAmong(const std::initializer_list<T> &values) |
||||
{ |
||||
const auto index { std::max<int32_t>(GenerateRnd(static_cast<int32_t>(values.size())), 0) }; |
||||
|
||||
return *(values.begin() + index); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random non-negative integer |
||||
* |
||||
* Effectively the same as GenerateRnd but will never return a negative value |
||||
* @param v upper limit for the return value |
||||
* @return a value between 0 and v-1 inclusive, i.e. the range [0, v) |
||||
*/ |
||||
inline int32_t RandomIntLessThan(int32_t v) |
||||
{ |
||||
return std::max<int32_t>(GenerateRnd(v), 0); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Randomly chooses a value somewhere within the given range |
||||
* @param min lower limit, minimum possible value |
||||
* @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range |
||||
* @param halfOpen whether to use the limits as a half-open range or not |
||||
* @return a randomly selected integer |
||||
*/ |
||||
inline int32_t RandomIntBetween(int32_t min, int32_t max, bool halfOpen = false) |
||||
{ |
||||
return RandomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min; |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
/**
|
||||
* @file random.hpp |
||||
* |
||||
* Contains convenience functions for random number generation |
||||
* |
||||
* This includes specific engine/distribution functions for logic that needs to be compatible with the base game. |
||||
*/ |
||||
#pragma once |
||||
|
||||
#include <algorithm> |
||||
#include <cstdint> |
||||
#include <initializer_list> |
||||
#include <random> |
||||
|
||||
namespace devilution { |
||||
|
||||
class DiabloGenerator { |
||||
private: |
||||
/** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */ |
||||
std::linear_congruential_engine<uint32_t, 0x015A4E35, 1, 0> lcg; |
||||
|
||||
public: |
||||
/**
|
||||
* @brief Set the state of the RandomNumberEngine used by the base game to the specific seed |
||||
* @param seed New engine state |
||||
*/ |
||||
DiabloGenerator(uint32_t seed) |
||||
{ |
||||
lcg.seed(seed); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Advance the global RandomNumberEngine state by the specified number of rounds |
||||
* |
||||
* Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated. |
||||
* @param count How many values to discard |
||||
*/ |
||||
void discardRandomValues(unsigned count) |
||||
{ |
||||
lcg.discard(count); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random non-negative integer (most of the time) using the vanilla RNG |
||||
* |
||||
* This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try |
||||
* discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31. |
||||
* |
||||
* This function is only used when the base game wants to store the seed used to generate an item or level, however |
||||
* as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more |
||||
* appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game. |
||||
* |
||||
* @return A random number in the range [0,2^31) or -2^31 |
||||
*/ |
||||
int32_t advanceRndSeed() |
||||
{ |
||||
const int32_t seed = static_cast<int32_t>(lcg()); |
||||
// since abs(INT_MIN) is undefined behavior, handle this value specially
|
||||
return seed == std::numeric_limits<int32_t>::min() ? std::numeric_limits<int32_t>::min() : std::abs(seed); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random integer less than the given limit using the vanilla RNG |
||||
* |
||||
* If v is not a positive number this function returns 0 without calling the RNG. |
||||
* |
||||
* Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value |
||||
* less than 32768 for limits in that range. |
||||
* |
||||
* This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed() |
||||
* |
||||
* @see AdvanceRndSeed() |
||||
* @param v The upper limit for the return value |
||||
* @return A random number in the range [0, v) or rarely a negative value in (-v, -1] |
||||
*/ |
||||
int32_t generateRnd(int32_t v) |
||||
{ |
||||
if (v <= 0) |
||||
return 0; |
||||
if (v <= 0x7FFF) // use the high bits to correct for LCG bias
|
||||
return (advanceRndSeed() >> 16) % v; |
||||
return advanceRndSeed() % v; |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random boolean value using the vanilla RNG |
||||
* |
||||
* This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2 |
||||
* represents a 50/50 chance. |
||||
* |
||||
* @param frequency odds of returning a true value |
||||
* @return A random boolean value |
||||
*/ |
||||
bool flipCoin(unsigned frequency) |
||||
{ |
||||
// Casting here because GenerateRnd takes a signed argument when it should take and yield unsigned.
|
||||
return generateRnd(static_cast<int32_t>(frequency)) == 0; |
||||
} |
||||
|
||||
/**
|
||||
* @brief Picks one of the elements in the list randomly. |
||||
* |
||||
* @param values The values to pick from |
||||
* @return A random value from the 'values' list. |
||||
*/ |
||||
template <typename T> |
||||
const T pickRandomlyAmong(const std::initializer_list<T> &values) |
||||
{ |
||||
const auto index { std::max<int32_t>(generateRnd(static_cast<int32_t>(values.size())), 0) }; |
||||
|
||||
return *(values.begin() + index); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random non-negative integer |
||||
* |
||||
* Effectively the same as GenerateRnd but will never return a negative value |
||||
* @param v upper limit for the return value |
||||
* @return a value between 0 and v-1 inclusive, i.e. the range [0, v) |
||||
*/ |
||||
inline int32_t randomIntLessThan(int32_t v) |
||||
{ |
||||
return std::max<int32_t>(generateRnd(v), 0); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Randomly chooses a value somewhere within the given range |
||||
* @param min lower limit, minimum possible value |
||||
* @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range |
||||
* @param halfOpen whether to use the limits as a half-open range or not |
||||
* @return a randomly selected integer |
||||
*/ |
||||
inline int32_t randomIntBetween(int32_t min, int32_t max, bool halfOpen = false) |
||||
{ |
||||
return randomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min; |
||||
} |
||||
}; |
||||
|
||||
// Based on fmix32 implementation from MurmurHash3 created by Austin Appleby in 2008
|
||||
// https://github.com/aappleby/smhasher/blob/61a0530f28277f2e850bfc39600ce61d02b518de/src/MurmurHash3.cpp#L68
|
||||
// and adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
|
||||
//
|
||||
// See also:
|
||||
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
|
||||
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
|
||||
// https://doi.org/10.1145/2714064.2660195
|
||||
class SplitMix32 { |
||||
uint32_t state; |
||||
|
||||
public: |
||||
SplitMix32(uint32_t state) |
||||
: state(state) |
||||
{ |
||||
} |
||||
|
||||
uint32_t next() |
||||
{ |
||||
uint32_t z = (state += 0x9e3779b9); |
||||
z = (z ^ (z >> 16)) * 0x85ebca6b; |
||||
z = (z ^ (z >> 13)) * 0xc2b2ae35; |
||||
return z ^ (z >> 16); |
||||
} |
||||
|
||||
void generate(uint32_t *begin, const uint32_t *end) |
||||
{ |
||||
while (begin != end) { |
||||
*begin = next(); |
||||
++begin; |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// Adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna
|
||||
//
|
||||
// See also:
|
||||
// Guy L. Steele, Doug Lea, and Christine H. Flood. 2014.
|
||||
// Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472.
|
||||
// https://doi.org/10.1145/2714064.2660195
|
||||
class SplitMix64 { |
||||
uint64_t state; |
||||
|
||||
public: |
||||
SplitMix64(uint64_t state) |
||||
: state(state) |
||||
{ |
||||
} |
||||
|
||||
uint64_t next() |
||||
{ |
||||
uint64_t z = (state += 0x9e3779b97f4a7c15); |
||||
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; |
||||
z = (z ^ (z >> 27)) * 0x94d049bb133111eb; |
||||
return z ^ (z >> 31); |
||||
} |
||||
|
||||
void generate(uint64_t *begin, const uint64_t *end) |
||||
{ |
||||
while (begin != end) { |
||||
*begin = next(); |
||||
++begin; |
||||
} |
||||
} |
||||
}; |
||||
|
||||
/** Adapted from https://prng.di.unimi.it/xoshiro128plusplus.c written in 2019 by David Blackman and Sebastiano Vigna */ |
||||
class xoshiro128plusplus { |
||||
public: |
||||
typedef uint32_t state[4]; |
||||
|
||||
xoshiro128plusplus() { seed(); } |
||||
xoshiro128plusplus(const state &s) { copy(this->s, s); } |
||||
xoshiro128plusplus(uint64_t initialSeed) { seed(initialSeed); } |
||||
xoshiro128plusplus(uint32_t initialSeed) { seed(initialSeed); } |
||||
|
||||
uint32_t next(); |
||||
|
||||
/* This is the jump function for the generator. It is equivalent
|
||||
to 2^64 calls to next(); it can be used to generate 2^64 |
||||
non-overlapping subsequences for parallel computations. */ |
||||
void jump() |
||||
{ |
||||
static constexpr uint32_t JUMP[] = { 0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b }; |
||||
|
||||
uint32_t s0 = 0; |
||||
uint32_t s1 = 0; |
||||
uint32_t s2 = 0; |
||||
uint32_t s3 = 0; |
||||
for (const uint32_t entry : JUMP) |
||||
for (int b = 0; b < 32; b++) { |
||||
if (entry & UINT32_C(1) << b) { |
||||
s0 ^= s[0]; |
||||
s1 ^= s[1]; |
||||
s2 ^= s[2]; |
||||
s3 ^= s[3]; |
||||
} |
||||
next(); |
||||
} |
||||
|
||||
s[0] = s0; |
||||
s[1] = s1; |
||||
s[2] = s2; |
||||
s[3] = s3; |
||||
} |
||||
|
||||
void save(state &s) const |
||||
{ |
||||
copy(s, this->s); |
||||
} |
||||
|
||||
private: |
||||
state s; |
||||
|
||||
void seed(uint64_t value) |
||||
{ |
||||
uint64_t seeds[2]; |
||||
SplitMix64 seedSequence { value }; |
||||
seedSequence.generate(seeds, seeds + 2); |
||||
|
||||
s[0] = static_cast<uint32_t>(seeds[0] >> 32); |
||||
s[1] = static_cast<uint32_t>(seeds[0]); |
||||
s[2] = static_cast<uint32_t>(seeds[1] >> 32); |
||||
s[3] = static_cast<uint32_t>(seeds[1]); |
||||
} |
||||
|
||||
void seed(uint32_t value) |
||||
{ |
||||
SplitMix32 seedSequence { value }; |
||||
seedSequence.generate(s, s + 4); |
||||
} |
||||
|
||||
void seed() |
||||
{ |
||||
seed(timeSeed()); |
||||
|
||||
#if !(defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0)) |
||||
static std::random_device rd; |
||||
std::uniform_int_distribution<uint32_t> dist; |
||||
for (uint32_t &cell : s) |
||||
cell ^= dist(rd); |
||||
#endif |
||||
} |
||||
|
||||
static uint64_t timeSeed(); |
||||
static void copy(state &dst, const state &src); |
||||
}; |
||||
|
||||
/**
|
||||
* @brief Returns a copy of the global seed generator and fast-forwards the global seed generator to avoid collisions |
||||
*/ |
||||
xoshiro128plusplus ReserveSeedSequence(); |
||||
|
||||
/**
|
||||
* @brief Advances the global seed generator state and returns the new value |
||||
*/ |
||||
uint32_t GenerateSeed(); |
||||
|
||||
/**
|
||||
* @brief Set the state of the RandomNumberEngine used by the base game to the specific seed |
||||
* @param seed New engine state |
||||
*/ |
||||
void SetRndSeed(uint32_t seed); |
||||
|
||||
/**
|
||||
* @brief Returns the current state of the RandomNumberEngine used by the base game |
||||
* |
||||
* This is only exposed to allow for debugging vanilla code and testing. Using this engine for new code is discouraged |
||||
* due to the poor randomness and bugs in the implementation that need to be retained for compatibility. |
||||
* |
||||
* @return The current engine state |
||||
*/ |
||||
uint32_t GetLCGEngineState(); |
||||
|
||||
/**
|
||||
* @brief Advance the global RandomNumberEngine state by the specified number of rounds |
||||
* |
||||
* Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated. |
||||
* @param count How many values to discard |
||||
*/ |
||||
void DiscardRandomValues(unsigned count); |
||||
|
||||
/**
|
||||
* @brief Advances the global RandomNumberEngine state and returns the new value |
||||
*/ |
||||
uint32_t GenerateRandomNumber(); |
||||
|
||||
/**
|
||||
* @brief Generates a random non-negative integer (most of the time) using the vanilla RNG |
||||
* |
||||
* This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try |
||||
* discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31. |
||||
* |
||||
* This function is only used when the base game wants to store the seed used to generate an item or level, however |
||||
* as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more |
||||
* appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game. |
||||
* |
||||
* @return A random number in the range [0,2^31) or -2^31 |
||||
*/ |
||||
[[nodiscard]] int32_t AdvanceRndSeed(); |
||||
|
||||
/**
|
||||
* @brief Generates a random integer less than the given limit using the vanilla RNG |
||||
* |
||||
* If v is not a positive number this function returns 0 without calling the RNG. |
||||
* |
||||
* Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value |
||||
* less than 32768 for limits in that range. |
||||
* |
||||
* This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed() |
||||
* |
||||
* @see AdvanceRndSeed() |
||||
* @param v The upper limit for the return value |
||||
* @return A random number in the range [0, v) or rarely a negative value in (-v, -1] |
||||
*/ |
||||
int32_t GenerateRnd(int32_t v); |
||||
|
||||
/**
|
||||
* @brief Generates a random boolean value using the vanilla RNG |
||||
* |
||||
* This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2 |
||||
* represents a 50/50 chance. |
||||
* |
||||
* @param frequency odds of returning a true value |
||||
* @return A random boolean value |
||||
*/ |
||||
bool FlipCoin(unsigned frequency = 2); |
||||
|
||||
/**
|
||||
* @brief Picks one of the elements in the list randomly. |
||||
* |
||||
* @param values The values to pick from |
||||
* @return A random value from the 'values' list. |
||||
*/ |
||||
template <typename T> |
||||
const T PickRandomlyAmong(const std::initializer_list<T> &values) |
||||
{ |
||||
const auto index { std::max<int32_t>(GenerateRnd(static_cast<int32_t>(values.size())), 0) }; |
||||
|
||||
return *(values.begin() + index); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Generates a random non-negative integer |
||||
* |
||||
* Effectively the same as GenerateRnd but will never return a negative value |
||||
* @param v upper limit for the return value |
||||
* @return a value between 0 and v-1 inclusive, i.e. the range [0, v) |
||||
*/ |
||||
inline int32_t RandomIntLessThan(int32_t v) |
||||
{ |
||||
return std::max<int32_t>(GenerateRnd(v), 0); |
||||
} |
||||
|
||||
/**
|
||||
* @brief Randomly chooses a value somewhere within the given range |
||||
* @param min lower limit, minimum possible value |
||||
* @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range |
||||
* @param halfOpen whether to use the limits as a half-open range or not |
||||
* @return a randomly selected integer |
||||
*/ |
||||
inline int32_t RandomIntBetween(int32_t min, int32_t max, bool halfOpen = false) |
||||
{ |
||||
return RandomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min; |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
#pragma once |
||||
|
||||
#include <string_view> |
||||
|
||||
namespace devilution { |
||||
|
||||
/**
|
||||
* @brief Triggers a Lua event by name. |
||||
* This is a minimal header for code that only needs to trigger events. |
||||
*/ |
||||
void LuaEvent(std::string_view name); |
||||
void LuaEvent(std::string_view name, std::string_view arg); |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,24 @@
|
||||
#include "lua/modules/floatingnumbers.hpp" |
||||
|
||||
#include <sol/sol.hpp> |
||||
|
||||
#include "engine/point.hpp" |
||||
#include "lua/metadoc.hpp" |
||||
#include "qol/floatingnumbers.h" |
||||
|
||||
namespace devilution { |
||||
|
||||
sol::table LuaFloatingNumbersModule(sol::state_view &lua) |
||||
{ |
||||
sol::table table = lua.create_table(); |
||||
|
||||
LuaSetDocFn(table, "add", "(text: string, pos: Point, style: UiFlags, id: integer = 0, reverseDirection: boolean = false)", |
||||
"Add a floating number", |
||||
[](const std::string &text, Point pos, UiFlags style, std::optional<int> id, std::optional<bool> reverseDirection) { |
||||
AddFloatingNumber(pos, { 0, 0 }, text, style, id.value_or(0), reverseDirection.value_or(false)); |
||||
}); |
||||
|
||||
return table; |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,9 @@
|
||||
#pragma once |
||||
|
||||
#include <sol/forward.hpp> |
||||
|
||||
namespace devilution { |
||||
|
||||
sol::table LuaFloatingNumbersModule(sol::state_view &lua); |
||||
|
||||
} // namespace devilution
|
||||
@ -1,9 +1,9 @@
|
||||
#pragma once |
||||
|
||||
#include <sol/sol.hpp> |
||||
|
||||
namespace devilution { |
||||
|
||||
sol::table LuaItemModule(sol::state_view &lua); |
||||
|
||||
} // namespace devilution
|
||||
#pragma once |
||||
|
||||
#include <sol/sol.hpp> |
||||
|
||||
namespace devilution { |
||||
|
||||
sol::table LuaItemModule(sol::state_view &lua); |
||||
|
||||
} // namespace devilution
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
#pragma once |
||||
|
||||
#include <sol/sol.hpp> |
||||
|
||||
namespace devilution { |
||||
|
||||
sol::table LuaMonstersModule(sol::state_view &lua); |
||||
|
||||
} // namespace devilution
|
||||
#pragma once |
||||
|
||||
#include <sol/sol.hpp> |
||||
|
||||
namespace devilution { |
||||
|
||||
sol::table LuaMonstersModule(sol::state_view &lua); |
||||
|
||||
} // namespace devilution
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
#include "lua/modules/system.hpp" |
||||
|
||||
#include <sol/sol.hpp> |
||||
|
||||
#ifdef USE_SDL3 |
||||
#include <SDL3/SDL_timer.h> |
||||
#else |
||||
#include <SDL.h> |
||||
#endif |
||||
|
||||
#include "lua/metadoc.hpp" |
||||
|
||||
namespace devilution { |
||||
|
||||
sol::table LuaSystemModule(sol::state_view &lua) |
||||
{ |
||||
sol::table table = lua.create_table(); |
||||
|
||||
LuaSetDocFn(table, "get_ticks", "() -> integer", "Returns the number of milliseconds since the game started.", |
||||
[]() { return static_cast<int>(SDL_GetTicks()); }); |
||||
|
||||
return table; |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,9 @@
|
||||
#pragma once |
||||
|
||||
#include <sol/sol.hpp> |
||||
|
||||
namespace devilution { |
||||
|
||||
sol::table LuaSystemModule(sol::state_view &lua); |
||||
|
||||
} // namespace devilution
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue