#include "panels/charpanel.hpp" #include #include #include #include #include #include #include "control/control.hpp" #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/algorithm/container.hpp" #include "utils/display.h" #include "utils/enum_traits.h" #include "utils/format_int.hpp" #include "utils/language.h" #include "utils/status_macros.hpp" #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> 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; const int current = InspectPlayer->GetCurrentAttributeValue(attr); const 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() { if (HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana)) return UiFlags::ColorRed; return InspectPlayer->_pMaxMana > InspectPlayer->_pMaxManaBase ? UiFlags::ColorBlue : UiFlags::ColorWhite; } UiFlags GetMaxHealthColor() { return InspectPlayer->_pMaxHP > InspectPlayer->_pMaxHPBase ? UiFlags::ColorBlue : UiFlags::ColorWhite; } std::pair 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; } const int mindam = InspectPlayer->_pIMinDam + InspectPlayer->_pIBonusDam * InspectPlayer->_pIMinDam / 100 + damageMod; const 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(InspectPlayer->getClassName()) }; } }, { N_("Level"), { 57, 52 }, 57, 45, []() { return StyledText { UiFlags::ColorWhite, StrCat(InspectPlayer->getCharacterLevel()) }; } }, { N_("Experience"), { TopRightLabelX, 52 }, 99, 91, []() { return StyledText { UiFlags::ColorWhite, FormatInteger(InspectPlayer->_pExperience) }; } }, { N_("Next level"), { TopRightLabelX, 80 }, 99, 198, []() { if (InspectPlayer->isMaxCharacterLevel()) { return StyledText { UiFlags::ColorWhitegold, std::string(_("None")) }; } const uint32_t nextExperienceThreshold = InspectPlayer->getNextExperienceThreshold(); return StyledText { UiFlags::ColorWhite, FormatInteger(nextExperienceThreshold) }; } }, { 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->getCharacterLevel() * 2) }; } }, { N_("Chance 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, []() { const auto [dmgMin, dmgMax] = GetDamage(); return StyledText { GetValueColor(InspectPlayer->_pIBonusDam), StrCat(dmgMin, "-", dmgMax) }; } }, { 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(HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana) ? 0 : InspectPlayer->_pMaxMana >> 6) }; } }, { "", { 135, 312 }, 45, 0, []() { return StyledText { (InspectPlayer->_pMana != InspectPlayer->_pMaxMana ? UiFlags::ColorRed : GetMaxManaColor()), StrCat((HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana) || InspectPlayer->hasNoMana()) ? 0 : 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 PanelFieldPaddingSide = 5; 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 std::string_view textStr = LanguageTranslate(entry.label); std::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 than the field, we center it vertically relative to the field. // Otherwise, we draw from the top of the field. const int textHeight = static_cast((c_count(wrapped, '\n') + 1) * GetLineHeight(wrapped, GameFont12)); const int labelHeight = std::max(PanelFieldHeight, textHeight); DrawString(out, text, { labelPosition + Displacement { -2, 2 }, { entry.labelLength, labelHeight } }, { .flags = style | UiFlags::ColorBlack, .spacing = Spacing }); DrawString(out, text, { labelPosition, { entry.labelLength, labelHeight } }, { .flags = style | UiFlags::ColorWhite, .spacing = 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)[CharPanelButton[static_cast(CharacterAttribute::Strength)] ? 2 : 1]); if (InspectPlayer->_pBaseMag < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Magic)) ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 185 }), (*pChrButtons)[CharPanelButton[static_cast(CharacterAttribute::Magic)] ? 4 : 3]); if (InspectPlayer->_pBaseDex < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Dexterity)) ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 214 }), (*pChrButtons)[CharPanelButton[static_cast(CharacterAttribute::Dexterity)] ? 6 : 5]); if (InspectPlayer->_pBaseVit < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Vitality)) ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 242 }), (*pChrButtons)[CharPanelButton[static_cast(CharacterAttribute::Vitality)] ? 8 : 7]); } } } // namespace tl::expected LoadCharPanel() { ASSIGN_OR_RETURN(OptionalOwnedClxSpriteList background, LoadClxWithStatus("data\\charbg.clx")); const OwnedSurface out((*background)[0].width(), (*background)[0].height()); RenderClxSprite(out, (*background)[0], { 0, 0 }); background = std::nullopt; { ASSIGN_OR_RETURN(OwnedClxSpriteList boxLeft, LoadClxWithStatus("data\\boxleftend.clx")); ASSIGN_OR_RETURN(OwnedClxSpriteList boxMiddle, LoadClxWithStatus("data\\boxmiddle.clx")); ASSIGN_OR_RETURN(OwnedClxSpriteList boxRight, LoadClxWithStatus("data\\boxrightend.clx")); const bool isSmallFontTall = IsSmallFontTall(); const int attributeHeadersY = isSmallFontTall ? 112 : 114; for (const 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); return {}; } void FreeCharPanel() { Panel = std::nullopt; } void DrawChr(const Surface &out) { const Point pos = GetPanelPosition(UiPanels::Character, { 0, 0 }); RenderClxSprite(out, (*Panel)[0], pos); for (auto &entry : panelEntries) { if (entry.statDisplayFunc) { const StyledText tmp = (*entry.statDisplayFunc)(); DrawString( out, tmp.text, { entry.position + Displacement { pos.x + PanelFieldPaddingSide, pos.y + PanelFieldPaddingTop }, { entry.length - (PanelFieldPaddingSide * 2), PanelFieldInnerHeight } }, { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter | UiFlags::VerticalCenter | tmp.style }); } } DrawStatButtons(out); } } // namespace devilution