You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
318 lines
13 KiB
318 lines
13 KiB
#include "panels/charpanel.hpp" |
|
|
|
#include <cstdint> |
|
|
|
#include <algorithm> |
|
#include <string> |
|
|
|
#include <fmt/format.h> |
|
#include <function_ref.hpp> |
|
|
|
#include "control.h" |
|
#include "engine/load_clx.hpp" |
|
#include "engine/render/clx_render.hpp" |
|
#include "engine/render/text_render.hpp" |
|
#include "panels/ui_panels.hpp" |
|
#include "player.h" |
|
#include "playerdat.hpp" |
|
#include "utils/display.h" |
|
#include "utils/format_int.hpp" |
|
#include "utils/language.h" |
|
#include "utils/str_cat.hpp" |
|
#include "utils/surface_to_clx.hpp" |
|
|
|
namespace devilution { |
|
|
|
OptionalOwnedClxSpriteList pChrButtons; |
|
|
|
namespace { |
|
|
|
struct StyledText { |
|
UiFlags style; |
|
std::string text; |
|
int spacing = 1; |
|
}; |
|
|
|
struct PanelEntry { |
|
std::string label; |
|
Point position; |
|
int length; |
|
int labelLength; // max label's length - used for line wrapping |
|
std::optional<tl::function_ref<StyledText()>> statDisplayFunc; // function responsible for displaying stat |
|
}; |
|
|
|
UiFlags GetBaseStatColor(CharacterAttribute attr) |
|
{ |
|
UiFlags style = UiFlags::ColorWhite; |
|
if (InspectPlayer->GetBaseAttributeValue(attr) == InspectPlayer->GetMaximumAttributeValue(attr)) |
|
style = UiFlags::ColorWhitegold; |
|
return style; |
|
} |
|
|
|
UiFlags GetCurrentStatColor(CharacterAttribute attr) |
|
{ |
|
UiFlags style = UiFlags::ColorWhite; |
|
int current = InspectPlayer->GetCurrentAttributeValue(attr); |
|
int base = InspectPlayer->GetBaseAttributeValue(attr); |
|
if (current > base) |
|
style = UiFlags::ColorBlue; |
|
if (current < base) |
|
style = UiFlags::ColorRed; |
|
return style; |
|
} |
|
|
|
UiFlags GetValueColor(int value, bool flip = false) |
|
{ |
|
UiFlags style = UiFlags::ColorWhite; |
|
if (value > 0) |
|
style = (flip ? UiFlags::ColorRed : UiFlags::ColorBlue); |
|
if (value < 0) |
|
style = (flip ? UiFlags::ColorBlue : UiFlags::ColorRed); |
|
return style; |
|
} |
|
|
|
UiFlags GetMaxManaColor() |
|
{ |
|
return InspectPlayer->_pMaxMana > InspectPlayer->_pMaxManaBase ? UiFlags::ColorBlue : UiFlags::ColorWhite; |
|
} |
|
|
|
UiFlags GetMaxHealthColor() |
|
{ |
|
return InspectPlayer->_pMaxHP > InspectPlayer->_pMaxHPBase ? UiFlags::ColorBlue : UiFlags::ColorWhite; |
|
} |
|
|
|
std::pair<int, int> GetDamage() |
|
{ |
|
int damageMod = InspectPlayer->_pIBonusDamMod; |
|
if (InspectPlayer->InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow && InspectPlayer->_pClass != HeroClass::Rogue) { |
|
damageMod += InspectPlayer->_pDamageMod / 2; |
|
} else { |
|
damageMod += InspectPlayer->_pDamageMod; |
|
} |
|
int mindam = InspectPlayer->_pIMinDam + InspectPlayer->_pIBonusDam * InspectPlayer->_pIMinDam / 100 + damageMod; |
|
int maxdam = InspectPlayer->_pIMaxDam + InspectPlayer->_pIBonusDam * InspectPlayer->_pIMaxDam / 100 + damageMod; |
|
return { mindam, maxdam }; |
|
} |
|
|
|
StyledText GetResistInfo(int8_t resist) |
|
{ |
|
UiFlags style = UiFlags::ColorBlue; |
|
if (resist == 0) |
|
style = UiFlags::ColorWhite; |
|
else if (resist < 0) |
|
style = UiFlags::ColorRed; |
|
else if (resist >= MaxResistance) |
|
style = UiFlags::ColorWhitegold; |
|
|
|
return { style, StrCat(resist, "%") }; |
|
} |
|
|
|
constexpr int LeftColumnLabelX = 88; |
|
constexpr int TopRightLabelX = 211; |
|
constexpr int RightColumnLabelX = 253; |
|
|
|
constexpr int LeftColumnLabelWidth = 76; |
|
constexpr int RightColumnLabelWidth = 68; |
|
|
|
// Indices in `panelEntries`. |
|
constexpr unsigned AttributeHeaderEntryIndices[2] = { 5, 6 }; |
|
constexpr unsigned GoldHeaderEntryIndex = 16; |
|
|
|
PanelEntry panelEntries[] = { |
|
{ "", { 9, 14 }, 150, 0, |
|
[]() { return StyledText { UiFlags::ColorWhite, InspectPlayer->_pName }; } }, |
|
{ "", { 161, 14 }, 149, 0, |
|
[]() { return StyledText { UiFlags::ColorWhite, std::string(_(PlayersData[static_cast<std::size_t>(InspectPlayer->_pClass)].className)) }; } }, |
|
|
|
{ N_("Level"), { 57, 52 }, 57, 45, |
|
[]() { return StyledText { UiFlags::ColorWhite, StrCat(InspectPlayer->_pLevel) }; } }, |
|
{ N_("Experience"), { TopRightLabelX, 52 }, 99, 91, |
|
[]() { |
|
int spacing = ((InspectPlayer->_pExperience >= 1000000000) ? 0 : 1); |
|
return StyledText { UiFlags::ColorWhite, FormatInteger(InspectPlayer->_pExperience), spacing }; |
|
} }, |
|
{ N_("Next level"), { TopRightLabelX, 80 }, 99, 198, |
|
[]() { |
|
if (InspectPlayer->_pLevel == MaxCharacterLevel) { |
|
return StyledText { UiFlags::ColorWhitegold, std::string(_("None")) }; |
|
} |
|
int spacing = ((InspectPlayer->_pNextExper >= 1000000000) ? 0 : 1); |
|
return StyledText { UiFlags::ColorWhite, FormatInteger(InspectPlayer->_pNextExper), spacing }; |
|
} }, |
|
|
|
{ N_("Base"), { LeftColumnLabelX, /* set dynamically */ 0 }, 0, 44, {} }, |
|
{ N_("Now"), { 135, /* set dynamically */ 0 }, 0, 44, {} }, |
|
{ N_("Strength"), { LeftColumnLabelX, 135 }, 45, LeftColumnLabelWidth, |
|
[]() { return StyledText { GetBaseStatColor(CharacterAttribute::Strength), StrCat(InspectPlayer->_pBaseStr) }; } }, |
|
{ "", { 135, 135 }, 45, 0, |
|
[]() { return StyledText { GetCurrentStatColor(CharacterAttribute::Strength), StrCat(InspectPlayer->_pStrength) }; } }, |
|
{ N_("Magic"), { LeftColumnLabelX, 163 }, 45, LeftColumnLabelWidth, |
|
[]() { return StyledText { GetBaseStatColor(CharacterAttribute::Magic), StrCat(InspectPlayer->_pBaseMag) }; } }, |
|
{ "", { 135, 163 }, 45, 0, |
|
[]() { return StyledText { GetCurrentStatColor(CharacterAttribute::Magic), StrCat(InspectPlayer->_pMagic) }; } }, |
|
{ N_("Dexterity"), { LeftColumnLabelX, 191 }, 45, LeftColumnLabelWidth, []() { return StyledText { GetBaseStatColor(CharacterAttribute::Dexterity), StrCat(InspectPlayer->_pBaseDex) }; } }, |
|
{ "", { 135, 191 }, 45, 0, |
|
[]() { return StyledText { GetCurrentStatColor(CharacterAttribute::Dexterity), StrCat(InspectPlayer->_pDexterity) }; } }, |
|
{ N_("Vitality"), { LeftColumnLabelX, 219 }, 45, LeftColumnLabelWidth, []() { return StyledText { GetBaseStatColor(CharacterAttribute::Vitality), StrCat(InspectPlayer->_pBaseVit) }; } }, |
|
{ "", { 135, 219 }, 45, 0, |
|
[]() { return StyledText { GetCurrentStatColor(CharacterAttribute::Vitality), StrCat(InspectPlayer->_pVitality) }; } }, |
|
{ N_("Points to distribute"), { LeftColumnLabelX, 248 }, 45, LeftColumnLabelWidth, |
|
[]() { |
|
InspectPlayer->_pStatPts = std::min(CalcStatDiff(*InspectPlayer), InspectPlayer->_pStatPts); |
|
return StyledText { UiFlags::ColorRed, (InspectPlayer->_pStatPts > 0 ? StrCat(InspectPlayer->_pStatPts) : "") }; |
|
} }, |
|
|
|
{ N_("Gold"), { TopRightLabelX, /* set dynamically */ 0 }, 0, 98, {} }, |
|
{ "", { TopRightLabelX, 127 }, 99, 0, |
|
[]() { return StyledText { UiFlags::ColorWhite, FormatInteger(InspectPlayer->_pGold) }; } }, |
|
|
|
{ N_("Armor class"), { RightColumnLabelX, 163 }, 57, RightColumnLabelWidth, |
|
[]() { return StyledText { GetValueColor(InspectPlayer->_pIBonusAC), StrCat(InspectPlayer->GetArmor() + InspectPlayer->_pLevel * 2) }; } }, |
|
{ N_("To hit"), { RightColumnLabelX, 191 }, 57, RightColumnLabelWidth, |
|
[]() { return StyledText { GetValueColor(InspectPlayer->_pIBonusToHit), StrCat(InspectPlayer->InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow ? InspectPlayer->GetRangedToHit() : InspectPlayer->GetMeleeToHit(), "%") }; } }, |
|
{ N_("Damage"), { RightColumnLabelX, 219 }, 57, RightColumnLabelWidth, |
|
[]() { |
|
std::pair<int, int> dmg = GetDamage(); |
|
int spacing = ((dmg.first >= 100) ? -1 : 1); |
|
return StyledText { GetValueColor(InspectPlayer->_pIBonusDam), StrCat(dmg.first, "-", dmg.second), spacing }; |
|
} }, |
|
|
|
{ N_("Life"), { LeftColumnLabelX, 284 }, 45, LeftColumnLabelWidth, |
|
[]() { return StyledText { GetMaxHealthColor(), StrCat(InspectPlayer->_pMaxHP >> 6) }; } }, |
|
{ "", { 135, 284 }, 45, 0, |
|
[]() { return StyledText { (InspectPlayer->_pHitPoints != InspectPlayer->_pMaxHP ? UiFlags::ColorRed : GetMaxHealthColor()), StrCat(InspectPlayer->_pHitPoints >> 6) }; } }, |
|
{ N_("Mana"), { LeftColumnLabelX, 312 }, 45, LeftColumnLabelWidth, |
|
[]() { return StyledText { GetMaxManaColor(), StrCat(InspectPlayer->_pMaxMana >> 6) }; } }, |
|
{ "", { 135, 312 }, 45, 0, |
|
[]() { return StyledText { (InspectPlayer->_pMana != InspectPlayer->_pMaxMana ? UiFlags::ColorRed : GetMaxManaColor()), StrCat(InspectPlayer->_pMana >> 6) }; } }, |
|
|
|
{ N_("Resist magic"), { RightColumnLabelX, 256 }, 57, RightColumnLabelWidth, |
|
[]() { return GetResistInfo(InspectPlayer->_pMagResist); } }, |
|
{ N_("Resist fire"), { RightColumnLabelX, 284 }, 57, RightColumnLabelWidth, |
|
[]() { return GetResistInfo(InspectPlayer->_pFireResist); } }, |
|
{ N_("Resist lightning"), { RightColumnLabelX, 313 }, 57, RightColumnLabelWidth, |
|
[]() { return GetResistInfo(InspectPlayer->_pLghtResist); } }, |
|
}; |
|
|
|
OptionalOwnedClxSpriteList Panel; |
|
|
|
constexpr int PanelFieldHeight = 24; |
|
constexpr int PanelFieldPaddingTop = 3; |
|
constexpr int PanelFieldPaddingBottom = 3; |
|
constexpr int PanelFieldInnerHeight = PanelFieldHeight - PanelFieldPaddingTop - PanelFieldPaddingBottom; |
|
|
|
void DrawPanelField(const Surface &out, Point pos, int len, ClxSprite left, ClxSprite middle, ClxSprite right) |
|
{ |
|
RenderClxSprite(out, left, pos); |
|
pos.x += left.width(); |
|
len -= left.width() + right.width(); |
|
RenderClxSprite(out.subregion(pos.x, pos.y, len, middle.height()), middle, Point { 0, 0 }); |
|
pos.x += len; |
|
RenderClxSprite(out, right, pos); |
|
} |
|
|
|
void DrawShadowString(const Surface &out, const PanelEntry &entry) |
|
{ |
|
if (entry.label.empty()) |
|
return; |
|
|
|
constexpr int Spacing = 0; |
|
const string_view textStr = LanguageTranslate(entry.label); |
|
string_view text; |
|
std::string wrapped; |
|
if (entry.labelLength > 0) { |
|
wrapped = WordWrapString(textStr, entry.labelLength, GameFont12, Spacing); |
|
text = wrapped; |
|
} else { |
|
text = textStr; |
|
} |
|
|
|
UiFlags style = UiFlags::VerticalCenter; |
|
|
|
Point labelPosition = entry.position; |
|
|
|
if (entry.length == 0) { |
|
style |= UiFlags::AlignCenter; |
|
} else { |
|
style |= UiFlags::AlignRight; |
|
labelPosition += Displacement { -entry.labelLength - (IsSmallFontTall() ? 2 : 3), 0 }; |
|
} |
|
|
|
// If the text is less tall then the field, we center it vertically relative to the field. |
|
// Otherwise, we draw from the top of the field. |
|
const int textHeight = (std::count(wrapped.begin(), wrapped.end(), '\n') + 1) * GetLineHeight(wrapped, GameFont12); |
|
const int labelHeight = std::max(PanelFieldHeight, textHeight); |
|
|
|
DrawString(out, text, { labelPosition + Displacement { -2, 2 }, { entry.labelLength, labelHeight } }, { style | UiFlags::ColorBlack, Spacing }); |
|
DrawString(out, text, { labelPosition, { entry.labelLength, labelHeight } }, { style | UiFlags::ColorWhite, Spacing }); |
|
} |
|
|
|
void DrawStatButtons(const Surface &out) |
|
{ |
|
if (InspectPlayer->_pStatPts > 0 && !IsInspectingPlayer()) { |
|
if (InspectPlayer->_pBaseStr < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Strength)) |
|
ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 157 }), (*pChrButtons)[chrbtn[static_cast<size_t>(CharacterAttribute::Strength)] ? 2 : 1]); |
|
if (InspectPlayer->_pBaseMag < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Magic)) |
|
ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 185 }), (*pChrButtons)[chrbtn[static_cast<size_t>(CharacterAttribute::Magic)] ? 4 : 3]); |
|
if (InspectPlayer->_pBaseDex < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Dexterity)) |
|
ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 214 }), (*pChrButtons)[chrbtn[static_cast<size_t>(CharacterAttribute::Dexterity)] ? 6 : 5]); |
|
if (InspectPlayer->_pBaseVit < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Vitality)) |
|
ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 242 }), (*pChrButtons)[chrbtn[static_cast<size_t>(CharacterAttribute::Vitality)] ? 8 : 7]); |
|
} |
|
} |
|
|
|
} // namespace |
|
|
|
void LoadCharPanel() |
|
{ |
|
OptionalOwnedClxSpriteList background = LoadClx("data\\charbg.clx"); |
|
OwnedSurface out((*background)[0].width(), (*background)[0].height()); |
|
RenderClxSprite(out, (*background)[0], { 0, 0 }); |
|
background = std::nullopt; |
|
|
|
{ |
|
OwnedClxSpriteList boxLeft = LoadClx("data\\boxleftend.clx"); |
|
OwnedClxSpriteList boxMiddle = LoadClx("data\\boxmiddle.clx"); |
|
OwnedClxSpriteList boxRight = LoadClx("data\\boxrightend.clx"); |
|
|
|
const bool isSmallFontTall = IsSmallFontTall(); |
|
const int attributeHeadersY = isSmallFontTall ? 112 : 114; |
|
for (unsigned i : AttributeHeaderEntryIndices) { |
|
panelEntries[i].position.y = attributeHeadersY; |
|
} |
|
panelEntries[GoldHeaderEntryIndex].position.y = isSmallFontTall ? 105 : 106; |
|
|
|
for (auto &entry : panelEntries) { |
|
if (entry.statDisplayFunc) { |
|
DrawPanelField(out, entry.position, entry.length, boxLeft[0], boxMiddle[0], boxRight[0]); |
|
} |
|
DrawShadowString(out, entry); |
|
} |
|
} |
|
|
|
Panel = SurfaceToClx(out); |
|
} |
|
|
|
void FreeCharPanel() |
|
{ |
|
Panel = std::nullopt; |
|
} |
|
|
|
void DrawChr(const Surface &out) |
|
{ |
|
Point pos = GetPanelPosition(UiPanels::Character, { 0, 0 }); |
|
RenderClxSprite(out, (*Panel)[0], pos); |
|
for (auto &entry : panelEntries) { |
|
if (entry.statDisplayFunc) { |
|
StyledText tmp = (*entry.statDisplayFunc)(); |
|
DrawString( |
|
out, |
|
tmp.text, |
|
{ entry.position + Displacement { pos.x, pos.y + PanelFieldPaddingTop }, { entry.length, PanelFieldInnerHeight } }, |
|
{ UiFlags::AlignCenter | UiFlags::VerticalCenter | tmp.style, tmp.spacing }); |
|
} |
|
} |
|
DrawStatButtons(out); |
|
} |
|
|
|
} // namespace devilution
|
|
|