| 1 | /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ |
| 2 | /* If you are missing that file, acquire a complete release at teeworlds.com. */ |
| 3 | #include "menus.h" |
| 4 | |
| 5 | #include <base/str.h> |
| 6 | |
| 7 | #include <engine/font_icons.h> |
| 8 | #include <engine/graphics.h> |
| 9 | #include <engine/shared/config.h> |
| 10 | #include <engine/storage.h> |
| 11 | #include <engine/textrender.h> |
| 12 | |
| 13 | #include <game/client/animstate.h> |
| 14 | #include <game/client/components/console.h> |
| 15 | #include <game/client/components/emoticon.h> |
| 16 | #include <game/client/components/skins.h> |
| 17 | #include <game/client/components/tooltips.h> |
| 18 | #include <game/client/gameclient.h> |
| 19 | #include <game/client/skin.h> |
| 20 | #include <game/client/ui.h> |
| 21 | #include <game/client/ui_listbox.h> |
| 22 | #include <game/localization.h> |
| 23 | |
| 24 | #include <algorithm> |
| 25 | #include <vector> |
| 26 | |
| 27 | void CMenus::(CUIRect MainView) |
| 28 | { |
| 29 | CUIRect TabBar, PlayerTab, DummyTab, ChangeInfo; |
| 30 | MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView); |
| 31 | TabBar.VSplitMid(pLeft: &TabBar, pRight: &ChangeInfo, Spacing: 20.f); |
| 32 | TabBar.VSplitMid(pLeft: &PlayerTab, pRight: &DummyTab); |
| 33 | MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView); |
| 34 | |
| 35 | static CButtonContainer s_PlayerTabButton; |
| 36 | if(DoButton_MenuTab(pButtonContainer: &s_PlayerTabButton, pText: Localize(pStr: "Player" ), Checked: !m_Dummy, pRect: &PlayerTab, Corners: IGraphics::CORNER_L, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f)) |
| 37 | { |
| 38 | m_Dummy = false; |
| 39 | m_SkinListScrollToSelected = true; |
| 40 | } |
| 41 | |
| 42 | static CButtonContainer s_DummyTabButton; |
| 43 | if(DoButton_MenuTab(pButtonContainer: &s_DummyTabButton, pText: Localize(pStr: "Dummy" ), Checked: m_Dummy, pRect: &DummyTab, Corners: IGraphics::CORNER_R, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f)) |
| 44 | { |
| 45 | m_Dummy = true; |
| 46 | m_SkinListScrollToSelected = true; |
| 47 | } |
| 48 | |
| 49 | if(Client()->State() == IClient::STATE_ONLINE && |
| 50 | GameClient()->m_aNextChangeInfo[m_Dummy] > Client()->GameTick(Conn: m_Dummy)) |
| 51 | { |
| 52 | char aChangeInfo[128], aTimeLeft[32]; |
| 53 | str_format(buffer: aTimeLeft, buffer_size: sizeof(aTimeLeft), format: Localize(pStr: "%ds left" ), (GameClient()->m_aNextChangeInfo[m_Dummy] - Client()->GameTick(Conn: m_Dummy) + Client()->GameTickSpeed() - 1) / Client()->GameTickSpeed()); |
| 54 | str_format(buffer: aChangeInfo, buffer_size: sizeof(aChangeInfo), format: "%s: %s" , Localize(pStr: "Player info change cooldown" ), aTimeLeft); |
| 55 | Ui()->DoLabel(pRect: &ChangeInfo, pText: aChangeInfo, Size: 10.f, Align: TEXTALIGN_ML); |
| 56 | } |
| 57 | |
| 58 | if(g_Config.m_Debug) |
| 59 | { |
| 60 | const CSkins::CSkinLoadingStats Stats = GameClient()->m_Skins.LoadingStats(); |
| 61 | char aStats[256]; |
| 62 | str_format(buffer: aStats, buffer_size: sizeof(aStats), format: "unloaded: %" PRIzu ", pending: %" PRIzu ", loading: %" PRIzu ",\nloaded: %" PRIzu ", error: %" PRIzu ", notfound: %" PRIzu, |
| 63 | Stats.m_NumUnloaded, Stats.m_NumPending, Stats.m_NumLoading, Stats.m_NumLoaded, Stats.m_NumError, Stats.m_NumNotFound); |
| 64 | Ui()->DoLabel(pRect: &ChangeInfo, pText: aStats, Size: 9.0f, Align: TEXTALIGN_MR); |
| 65 | } |
| 66 | |
| 67 | char *pSkinName; |
| 68 | size_t SkinNameSize; |
| 69 | int *pUseCustomColor; |
| 70 | unsigned *pColorBody; |
| 71 | unsigned *pColorFeet; |
| 72 | int *pEmote; |
| 73 | if(!m_Dummy) |
| 74 | { |
| 75 | pSkinName = g_Config.m_ClPlayerSkin; |
| 76 | SkinNameSize = sizeof(g_Config.m_ClPlayerSkin); |
| 77 | pUseCustomColor = &g_Config.m_ClPlayerUseCustomColor; |
| 78 | pColorBody = &g_Config.m_ClPlayerColorBody; |
| 79 | pColorFeet = &g_Config.m_ClPlayerColorFeet; |
| 80 | pEmote = &g_Config.m_ClPlayerDefaultEyes; |
| 81 | } |
| 82 | else |
| 83 | { |
| 84 | pSkinName = g_Config.m_ClDummySkin; |
| 85 | SkinNameSize = sizeof(g_Config.m_ClDummySkin); |
| 86 | pUseCustomColor = &g_Config.m_ClDummyUseCustomColor; |
| 87 | pColorBody = &g_Config.m_ClDummyColorBody; |
| 88 | pColorFeet = &g_Config.m_ClDummyColorFeet; |
| 89 | pEmote = &g_Config.m_ClDummyDefaultEyes; |
| 90 | } |
| 91 | |
| 92 | const float EyeButtonSize = 40.0f; |
| 93 | const bool RenderEyesBelow = MainView.w < 750.0f; |
| 94 | CUIRect YourSkin, Checkboxes, SkinPrefix, Eyes, Button, Label; |
| 95 | MainView.HSplitTop(Cut: 90.0f, pTop: &YourSkin, pBottom: &MainView); |
| 96 | if(RenderEyesBelow) |
| 97 | { |
| 98 | YourSkin.VSplitLeft(Cut: MainView.w * 0.45f, pLeft: &YourSkin, pRight: &Checkboxes); |
| 99 | Checkboxes.VSplitLeft(Cut: MainView.w * 0.35f, pLeft: &Checkboxes, pRight: &SkinPrefix); |
| 100 | MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView); |
| 101 | MainView.HSplitTop(Cut: EyeButtonSize, pTop: &Eyes, pBottom: &MainView); |
| 102 | Eyes.VSplitRight(Cut: EyeButtonSize * (float)NUM_EMOTES + 5.0f * (float)(NUM_EMOTES - 1), pLeft: nullptr, pRight: &Eyes); |
| 103 | } |
| 104 | else |
| 105 | { |
| 106 | YourSkin.VSplitRight(Cut: 3 * EyeButtonSize + 2 * 5.0f, pLeft: &YourSkin, pRight: &Eyes); |
| 107 | const float RemainderWidth = YourSkin.w; |
| 108 | YourSkin.VSplitLeft(Cut: RemainderWidth * 0.4f, pLeft: &YourSkin, pRight: &Checkboxes); |
| 109 | Checkboxes.VSplitLeft(Cut: RemainderWidth * 0.35f, pLeft: &Checkboxes, pRight: &SkinPrefix); |
| 110 | SkinPrefix.VSplitRight(Cut: 20.0f, pLeft: &SkinPrefix, pRight: nullptr); |
| 111 | } |
| 112 | YourSkin.VSplitRight(Cut: 20.0f, pLeft: &YourSkin, pRight: nullptr); |
| 113 | Checkboxes.VSplitRight(Cut: 20.0f, pLeft: &Checkboxes, pRight: nullptr); |
| 114 | |
| 115 | // Checkboxes |
| 116 | bool ShouldRefresh = false; |
| 117 | Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes); |
| 118 | if(DoButton_CheckBox(pId: &g_Config.m_ClDownloadSkins, pText: Localize(pStr: "Download skins" ), Checked: g_Config.m_ClDownloadSkins, pRect: &Button)) |
| 119 | { |
| 120 | g_Config.m_ClDownloadSkins ^= 1; |
| 121 | ShouldRefresh = true; |
| 122 | } |
| 123 | |
| 124 | Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes); |
| 125 | if(DoButton_CheckBox(pId: &g_Config.m_ClDownloadCommunitySkins, pText: Localize(pStr: "Download community skins" ), Checked: g_Config.m_ClDownloadCommunitySkins, pRect: &Button)) |
| 126 | { |
| 127 | g_Config.m_ClDownloadCommunitySkins ^= 1; |
| 128 | ShouldRefresh = true; |
| 129 | } |
| 130 | |
| 131 | Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes); |
| 132 | if(DoButton_CheckBox(pId: &g_Config.m_ClVanillaSkinsOnly, pText: Localize(pStr: "Vanilla skins only" ), Checked: g_Config.m_ClVanillaSkinsOnly, pRect: &Button)) |
| 133 | { |
| 134 | g_Config.m_ClVanillaSkinsOnly ^= 1; |
| 135 | ShouldRefresh = true; |
| 136 | } |
| 137 | |
| 138 | Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes); |
| 139 | if(DoButton_CheckBox(pId: &g_Config.m_ClFatSkins, pText: Localize(pStr: "Fat skins (DDFat)" ), Checked: g_Config.m_ClFatSkins, pRect: &Button)) |
| 140 | { |
| 141 | g_Config.m_ClFatSkins ^= 1; |
| 142 | } |
| 143 | |
| 144 | // Skin prefix |
| 145 | { |
| 146 | SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &SkinPrefix); |
| 147 | Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Skin prefix" ), Size: 14.0f, Align: TEXTALIGN_ML); |
| 148 | |
| 149 | SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &SkinPrefix); |
| 150 | static CLineInput s_SkinPrefixInput(g_Config.m_ClSkinPrefix, sizeof(g_Config.m_ClSkinPrefix)); |
| 151 | if(Ui()->DoClearableEditBox(pLineInput: &s_SkinPrefixInput, pRect: &Button, FontSize: 14.0f)) |
| 152 | { |
| 153 | ShouldRefresh = true; |
| 154 | } |
| 155 | |
| 156 | SkinPrefix.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &SkinPrefix); |
| 157 | |
| 158 | static const char *s_apSkinPrefixes[] = {"kitty" , "santa" }; |
| 159 | static CButtonContainer s_aPrefixButtons[std::size(s_apSkinPrefixes)]; |
| 160 | for(size_t i = 0; i < std::size(s_apSkinPrefixes); i++) |
| 161 | { |
| 162 | SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &SkinPrefix); |
| 163 | Button.HMargin(Cut: 2.0f, pOtherRect: &Button); |
| 164 | if(DoButton_Menu(pButtonContainer: &s_aPrefixButtons[i], pText: s_apSkinPrefixes[i], Checked: 0, pRect: &Button)) |
| 165 | { |
| 166 | str_copy(dst&: g_Config.m_ClSkinPrefix, src: s_apSkinPrefixes[i]); |
| 167 | ShouldRefresh = true; |
| 168 | } |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | // Player skin area |
| 173 | CUIRect CustomColorsButton, RandomSkinButton; |
| 174 | YourSkin.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &YourSkin); |
| 175 | YourSkin.HSplitBottom(Cut: 20.0f, pTop: &YourSkin, pBottom: &CustomColorsButton); |
| 176 | CustomColorsButton.VSplitRight(Cut: 30.0f, pLeft: &CustomColorsButton, pRight: &RandomSkinButton); |
| 177 | CustomColorsButton.VSplitRight(Cut: 20.0f, pLeft: &CustomColorsButton, pRight: nullptr); |
| 178 | YourSkin.VSplitLeft(Cut: 65.0f, pLeft: &YourSkin, pRight: &Button); |
| 179 | Button.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Button); |
| 180 | Button.HMargin(Cut: (Button.h - 20.0f) / 2.0f, pOtherRect: &Button); |
| 181 | |
| 182 | char aBuf[128 + IO_MAX_PATH_LENGTH]; |
| 183 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:" , Localize(pStr: "Your skin" )); |
| 184 | Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML); |
| 185 | |
| 186 | CSkins::CSkinList &SkinList = GameClient()->m_Skins.SkinList(); |
| 187 | const CSkin *pDefaultSkin = GameClient()->m_Skins.Find(pName: "default" ); |
| 188 | const CSkins::CSkinContainer *pOwnSkinContainer = GameClient()->m_Skins.FindContainerOrNullptr(pName: pSkinName[0] == '\0' ? "default" : pSkinName); |
| 189 | if(pOwnSkinContainer != nullptr && pOwnSkinContainer->IsSpecial()) |
| 190 | { |
| 191 | pOwnSkinContainer = nullptr; // Special skins cannot be selected, show as missing due to invalid name |
| 192 | } |
| 193 | |
| 194 | CTeeRenderInfo OwnSkinInfo; |
| 195 | OwnSkinInfo.Apply(pSkin: pOwnSkinContainer == nullptr || pOwnSkinContainer->Skin() == nullptr ? pDefaultSkin : pOwnSkinContainer->Skin().get()); |
| 196 | OwnSkinInfo.ApplyColors(CustomColoredSkin: *pUseCustomColor, ColorBody: *pColorBody, ColorFeet: *pColorFeet); |
| 197 | OwnSkinInfo.m_Size = 50.0f; |
| 198 | |
| 199 | // Tee |
| 200 | { |
| 201 | vec2 OffsetToMid; |
| 202 | CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, TeeOffsetToMid&: OffsetToMid); |
| 203 | const vec2 TeeRenderPos = vec2(YourSkin.x + YourSkin.w / 2.0f, YourSkin.y + YourSkin.h / 2.0f + OffsetToMid.y); |
| 204 | // tee looking towards cursor, and it is happy when you touch it |
| 205 | const vec2 DeltaPosition = Ui()->MousePos() - TeeRenderPos; |
| 206 | const float Distance = length(a: DeltaPosition); |
| 207 | const float InteractionDistance = 20.0f; |
| 208 | const vec2 TeeDirection = Distance < InteractionDistance ? normalize(v: vec2(DeltaPosition.x, std::max(a: DeltaPosition.y, b: 0.5f))) : normalize(v: DeltaPosition); |
| 209 | const int TeeEmote = Distance < InteractionDistance ? EMOTE_HAPPY : *pEmote; |
| 210 | RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: TeeEmote, Dir: TeeDirection, Pos: TeeRenderPos); |
| 211 | } |
| 212 | |
| 213 | // Skin loading status |
| 214 | const auto &&RenderSkinStatus = [&](CUIRect Parent, const CSkins::CSkinContainer *pSkinContainer, const void *pStatusTooltipId) { |
| 215 | if(pSkinContainer != nullptr && pSkinContainer->State() == CSkins::CSkinContainer::EState::LOADED) |
| 216 | { |
| 217 | return; |
| 218 | } |
| 219 | |
| 220 | CUIRect StatusIcon; |
| 221 | Parent.HSplitTop(Cut: 20.0f, pTop: &StatusIcon, pBottom: nullptr); |
| 222 | StatusIcon.VSplitLeft(Cut: 20.0f, pLeft: &StatusIcon, pRight: nullptr); |
| 223 | |
| 224 | if(pSkinContainer != nullptr && |
| 225 | (pSkinContainer->State() == CSkins::CSkinContainer::EState::UNLOADED || |
| 226 | pSkinContainer->State() == CSkins::CSkinContainer::EState::PENDING || |
| 227 | pSkinContainer->State() == CSkins::CSkinContainer::EState::LOADING)) |
| 228 | { |
| 229 | Ui()->RenderProgressSpinner(Center: StatusIcon.Center(), OuterRadius: 5.0f); |
| 230 | } |
| 231 | else |
| 232 | { |
| 233 | TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f)); |
| 234 | TextRender()->SetFontPreset(EFontPreset::ICON_FONT); |
| 235 | TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); |
| 236 | Ui()->DoLabel(pRect: &StatusIcon, pText: pSkinContainer == nullptr || pSkinContainer->State() == CSkins::CSkinContainer::EState::ERROR ? FontIcon::TRIANGLE_EXCLAMATION : FontIcon::QUESTION, Size: 12.0f, Align: TEXTALIGN_MC); |
| 237 | TextRender()->SetRenderFlags(0); |
| 238 | TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); |
| 239 | TextRender()->TextColor(Color: TextRender()->DefaultTextColor()); |
| 240 | Ui()->DoButtonLogic(pId: pStatusTooltipId, Checked: 0, pRect: &StatusIcon, Flags: BUTTONFLAG_NONE); |
| 241 | const char *pErrorTooltip; |
| 242 | if(pSkinContainer == nullptr) |
| 243 | { |
| 244 | pErrorTooltip = Localize(pStr: "This skin name cannot be used." ); |
| 245 | } |
| 246 | else if(pSkinContainer->State() == CSkins::CSkinContainer::EState::ERROR) |
| 247 | { |
| 248 | pErrorTooltip = Localize(pStr: "Skin could not be loaded due to an error. Check the local console for details." ); |
| 249 | } |
| 250 | else |
| 251 | { |
| 252 | pErrorTooltip = Localize(pStr: "Skin could not be found." ); |
| 253 | } |
| 254 | GameClient()->m_Tooltips.DoToolTip(pId: pStatusTooltipId, pNearRect: &StatusIcon, pText: pErrorTooltip); |
| 255 | } |
| 256 | }; |
| 257 | static char s_StatusTooltipId; |
| 258 | RenderSkinStatus(YourSkin, pOwnSkinContainer, &s_StatusTooltipId); |
| 259 | |
| 260 | // Skin name |
| 261 | static CLineInput s_SkinInput; |
| 262 | s_SkinInput.SetBuffer(pStr: pSkinName, MaxSize: SkinNameSize); |
| 263 | s_SkinInput.SetEmptyText("default" ); |
| 264 | if(Ui()->DoClearableEditBox(pLineInput: &s_SkinInput, pRect: &Button, FontSize: 14.0f)) |
| 265 | { |
| 266 | SetNeedSendInfo(); |
| 267 | m_SkinListScrollToSelected = true; |
| 268 | SkinList.ForceRefresh(); |
| 269 | } |
| 270 | |
| 271 | // Random skin button |
| 272 | static CButtonContainer s_RandomSkinButton; |
| 273 | static const char *s_apDice[] = {FontIcon::DICE_ONE, FontIcon::DICE_TWO, FontIcon::DICE_THREE, FontIcon::DICE_FOUR, FontIcon::DICE_FIVE, FontIcon::DICE_SIX}; |
| 274 | static int s_CurrentDie = rand() % std::size(s_apDice); |
| 275 | TextRender()->SetFontPreset(EFontPreset::ICON_FONT); |
| 276 | TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); |
| 277 | if(DoButton_Menu(pButtonContainer: &s_RandomSkinButton, pText: s_apDice[s_CurrentDie], Checked: 0, pRect: &RandomSkinButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: -0.2f)) |
| 278 | { |
| 279 | GameClient()->m_Skins.RandomizeSkin(Dummy: m_Dummy); |
| 280 | SetNeedSendInfo(); |
| 281 | m_SkinListScrollToSelected = true; |
| 282 | s_CurrentDie = rand() % std::size(s_apDice); |
| 283 | } |
| 284 | TextRender()->SetRenderFlags(0); |
| 285 | TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); |
| 286 | GameClient()->m_Tooltips.DoToolTip(pId: &s_RandomSkinButton, pNearRect: &RandomSkinButton, pText: Localize(pStr: "Create a random skin" )); |
| 287 | |
| 288 | // Custom colors button |
| 289 | if(DoButton_CheckBox(pId: pUseCustomColor, pText: Localize(pStr: "Custom colors" ), Checked: *pUseCustomColor, pRect: &CustomColorsButton)) |
| 290 | { |
| 291 | *pUseCustomColor = *pUseCustomColor ? 0 : 1; |
| 292 | SetNeedSendInfo(); |
| 293 | } |
| 294 | |
| 295 | // Default eyes |
| 296 | { |
| 297 | CTeeRenderInfo EyeSkinInfo = OwnSkinInfo; |
| 298 | EyeSkinInfo.m_Size = EyeButtonSize; |
| 299 | vec2 OffsetToMid; |
| 300 | CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &EyeSkinInfo, TeeOffsetToMid&: OffsetToMid); |
| 301 | |
| 302 | CUIRect EyesRow; |
| 303 | Eyes.HSplitTop(Cut: EyeButtonSize, pTop: &EyesRow, pBottom: &Eyes); |
| 304 | static CButtonContainer s_aEyeButtons[NUM_EMOTES]; |
| 305 | for(int CurrentEyeEmote = 0; CurrentEyeEmote < NUM_EMOTES; CurrentEyeEmote++) |
| 306 | { |
| 307 | EyesRow.VSplitLeft(Cut: EyeButtonSize, pLeft: &Button, pRight: &EyesRow); |
| 308 | EyesRow.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &EyesRow); |
| 309 | if(!RenderEyesBelow && (CurrentEyeEmote + 1) % 3 == 0) |
| 310 | { |
| 311 | Eyes.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Eyes); |
| 312 | Eyes.HSplitTop(Cut: EyeButtonSize, pTop: &EyesRow, pBottom: &Eyes); |
| 313 | } |
| 314 | |
| 315 | const ColorRGBA EyeButtonColor = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f + (*pEmote == CurrentEyeEmote ? 0.25f : 0.0f)); |
| 316 | if(DoButton_Menu(pButtonContainer: &s_aEyeButtons[CurrentEyeEmote], pText: "" , Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: EyeButtonColor)) |
| 317 | { |
| 318 | *pEmote = CurrentEyeEmote; |
| 319 | if((int)m_Dummy == g_Config.m_ClDummy) |
| 320 | GameClient()->m_Emoticon.EyeEmote(EyeEmote: CurrentEyeEmote); |
| 321 | } |
| 322 | GameClient()->m_Tooltips.DoToolTip(pId: &s_aEyeButtons[CurrentEyeEmote], pNearRect: &Button, pText: Localize(pStr: "Choose default eyes when joining a server" )); |
| 323 | RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &EyeSkinInfo, Emote: CurrentEyeEmote, Dir: vec2(1.0f, 0.0f), Pos: vec2(Button.x + Button.w / 2.0f, Button.y + Button.h / 2.0f + OffsetToMid.y)); |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | // Custom color pickers |
| 328 | MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView); |
| 329 | if(*pUseCustomColor) |
| 330 | { |
| 331 | CUIRect CustomColors; |
| 332 | MainView.HSplitTop(Cut: 95.0f, pTop: &CustomColors, pBottom: &MainView); |
| 333 | CUIRect aRects[2]; |
| 334 | CustomColors.VSplitMid(pLeft: &aRects[0], pRight: &aRects[1], Spacing: 20.0f); |
| 335 | |
| 336 | unsigned *apColors[] = {pColorBody, pColorFeet}; |
| 337 | const char *apParts[] = {Localize(pStr: "Body" ), Localize(pStr: "Feet" )}; |
| 338 | |
| 339 | for(int i = 0; i < 2; i++) |
| 340 | { |
| 341 | aRects[i].HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &aRects[i]); |
| 342 | Ui()->DoLabel(pRect: &Label, pText: apParts[i], Size: 14.0f, Align: TEXTALIGN_ML); |
| 343 | if(RenderHslaScrollbars(pRect: &aRects[i], pColor: apColors[i], Alpha: false, DarkestLight: ColorHSLA::DARKEST_LGT)) |
| 344 | { |
| 345 | SetNeedSendInfo(); |
| 346 | } |
| 347 | } |
| 348 | } |
| 349 | MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView); |
| 350 | |
| 351 | // Layout bottom controls and use remainder for skin selector |
| 352 | CUIRect QuickSearch, DatabaseButton, DirectoryButton, RefreshButton; |
| 353 | MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &QuickSearch); |
| 354 | MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr); |
| 355 | QuickSearch.VSplitLeft(Cut: 220.0f, pLeft: &QuickSearch, pRight: &DatabaseButton); |
| 356 | DatabaseButton.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &DatabaseButton); |
| 357 | DatabaseButton.VSplitLeft(Cut: 150.0f, pLeft: &DatabaseButton, pRight: &DirectoryButton); |
| 358 | DirectoryButton.VSplitRight(Cut: 175.0f, pLeft: nullptr, pRight: &DirectoryButton); |
| 359 | DirectoryButton.VSplitRight(Cut: 25.0f, pLeft: &DirectoryButton, pRight: &RefreshButton); |
| 360 | DirectoryButton.VSplitRight(Cut: 10.0f, pLeft: &DirectoryButton, pRight: nullptr); |
| 361 | |
| 362 | // Skin selector |
| 363 | static CListBox s_ListBox; |
| 364 | std::vector<CSkins::CSkinListEntry> &vSkinList = SkinList.Skins(); |
| 365 | int OldSelected = -1; |
| 366 | s_ListBox.DoStart(RowHeight: 50.0f, NumItems: vSkinList.size(), ItemsPerRow: 4, RowsPerScroll: 2, SelectedIndex: OldSelected, pRect: &MainView); |
| 367 | for(size_t i = 0; i < vSkinList.size(); ++i) |
| 368 | { |
| 369 | CSkins::CSkinListEntry &SkinListEntry = vSkinList[i]; |
| 370 | const CSkins::CSkinContainer *pSkinContainer = vSkinList[i].SkinContainer(); |
| 371 | |
| 372 | if(!m_Dummy ? SkinListEntry.IsSelectedMain() : SkinListEntry.IsSelectedDummy()) |
| 373 | { |
| 374 | OldSelected = i; |
| 375 | if(m_SkinListScrollToSelected) |
| 376 | { |
| 377 | s_ListBox.ScrollToSelected(); |
| 378 | m_SkinListScrollToSelected = false; |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | const CListboxItem Item = s_ListBox.DoNextItem(pId: SkinListEntry.ListItemId(), Selected: OldSelected >= 0 && (size_t)OldSelected == i); |
| 383 | if(!Item.m_Visible) |
| 384 | { |
| 385 | continue; |
| 386 | } |
| 387 | |
| 388 | SkinListEntry.RequestLoad(); |
| 389 | const CSkin *pSkin = pSkinContainer->State() == CSkins::CSkinContainer::EState::LOADED ? pSkinContainer->Skin().get() : pDefaultSkin; |
| 390 | |
| 391 | Item.m_Rect.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &Label); |
| 392 | |
| 393 | { |
| 394 | CTeeRenderInfo Info = OwnSkinInfo; |
| 395 | Info.Apply(pSkin); |
| 396 | vec2 OffsetToMid; |
| 397 | CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, TeeOffsetToMid&: OffsetToMid); |
| 398 | const vec2 TeeRenderPos = vec2(Button.x + Button.w / 2.0f, Button.y + Button.h / 2 + OffsetToMid.y); |
| 399 | RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, Emote: *pEmote, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos); |
| 400 | } |
| 401 | |
| 402 | { |
| 403 | SLabelProperties Props; |
| 404 | Props.m_MaxWidth = Label.w - 5.0f; |
| 405 | const auto &NameMatch = SkinListEntry.NameMatch(); |
| 406 | if(NameMatch.has_value()) |
| 407 | { |
| 408 | const auto [MatchStart, MatchLength] = NameMatch.value(); |
| 409 | Props.m_vColorSplits.emplace_back(args: MatchStart, args: MatchLength, args: ColorRGBA(0.4f, 0.4f, 1.0f, 1.0f)); |
| 410 | } |
| 411 | Ui()->DoLabel(pRect: &Label, pText: pSkinContainer->Name(), Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props); |
| 412 | } |
| 413 | |
| 414 | if(g_Config.m_Debug) |
| 415 | { |
| 416 | Graphics()->TextureClear(); |
| 417 | Graphics()->QuadsBegin(); |
| 418 | Graphics()->SetColor(*pUseCustomColor ? color_cast<ColorRGBA>(hsl: ColorHSLA(*pColorBody).UnclampLighting(Darkest: ColorHSLA::DARKEST_LGT)) : pSkin->m_BloodColor); |
| 419 | IGraphics::CQuadItem QuadItem(Label.x, Label.y, 12.0f, 12.0f); |
| 420 | Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1); |
| 421 | Graphics()->QuadsEnd(); |
| 422 | } |
| 423 | |
| 424 | // render skin favorite icon |
| 425 | { |
| 426 | CUIRect FavIcon; |
| 427 | Item.m_Rect.HSplitTop(Cut: 20.0f, pTop: &FavIcon, pBottom: nullptr); |
| 428 | FavIcon.VSplitRight(Cut: 20.0f, pLeft: nullptr, pRight: &FavIcon); |
| 429 | if(DoButton_Favorite(pButtonId: SkinListEntry.FavoriteButtonId(), pParentId: SkinListEntry.ListItemId(), Checked: SkinListEntry.IsFavorite(), pRect: &FavIcon)) |
| 430 | { |
| 431 | if(SkinListEntry.IsFavorite()) |
| 432 | { |
| 433 | GameClient()->m_Skins.RemoveFavorite(pName: pSkinContainer->Name()); |
| 434 | } |
| 435 | else |
| 436 | { |
| 437 | GameClient()->m_Skins.AddFavorite(pName: pSkinContainer->Name()); |
| 438 | } |
| 439 | } |
| 440 | } |
| 441 | |
| 442 | RenderSkinStatus(Item.m_Rect, pSkinContainer, SkinListEntry.ErrorTooltipId()); |
| 443 | } |
| 444 | |
| 445 | const int NewSelected = s_ListBox.DoEnd(); |
| 446 | if(OldSelected != NewSelected) |
| 447 | { |
| 448 | str_copy(dst: pSkinName, src: vSkinList[NewSelected].SkinContainer()->Name(), dst_size: SkinNameSize); |
| 449 | SkinList.ForceRefresh(); |
| 450 | SetNeedSendInfo(); |
| 451 | } |
| 452 | |
| 453 | static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString)); |
| 454 | if(SkinList.UnfilteredCount() > 0 && vSkinList.empty()) |
| 455 | { |
| 456 | CUIRect FilterLabel, ResetButton; |
| 457 | MainView.HMargin(Cut: (MainView.h - (16.0f + 18.0f + 8.0f)) / 2.0f, pOtherRect: &FilterLabel); |
| 458 | FilterLabel.HSplitTop(Cut: 16.0f, pTop: &FilterLabel, pBottom: &ResetButton); |
| 459 | ResetButton.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ResetButton); |
| 460 | ResetButton.VMargin(Cut: (ResetButton.w - 200.0f) / 2.0f, pOtherRect: &ResetButton); |
| 461 | Ui()->DoLabel(pRect: &FilterLabel, pText: Localize(pStr: "No skins match your filter criteria" ), Size: 16.0f, Align: TEXTALIGN_MC); |
| 462 | static CButtonContainer s_ResetButton; |
| 463 | if(DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset filter" ), Checked: 0, pRect: &ResetButton)) |
| 464 | { |
| 465 | s_SkinFilterInput.Clear(); |
| 466 | SkinList.ForceRefresh(); |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | if(Ui()->DoEditBox_Search(pLineInput: &s_SkinFilterInput, pRect: &QuickSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive())) |
| 471 | { |
| 472 | SkinList.ForceRefresh(); |
| 473 | } |
| 474 | |
| 475 | static CButtonContainer s_SkinDatabaseButton; |
| 476 | if(DoButton_Menu(pButtonContainer: &s_SkinDatabaseButton, pText: Localize(pStr: "Skin Database" ), Checked: 0, pRect: &DatabaseButton)) |
| 477 | { |
| 478 | Client()->ViewLink(pLink: "https://ddnet.org/skins/" ); |
| 479 | } |
| 480 | |
| 481 | static CButtonContainer s_DirectoryButton; |
| 482 | if(DoButton_Menu(pButtonContainer: &s_DirectoryButton, pText: Localize(pStr: "Skins directory" ), Checked: 0, pRect: &DirectoryButton)) |
| 483 | { |
| 484 | Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "skins" , pBuffer: aBuf, BufferSize: sizeof(aBuf)); |
| 485 | Storage()->CreateFolder(pFoldername: "skins" , Type: IStorage::TYPE_SAVE); |
| 486 | Client()->ViewFile(pFilename: aBuf); |
| 487 | } |
| 488 | GameClient()->m_Tooltips.DoToolTip(pId: &s_DirectoryButton, pNearRect: &DirectoryButton, pText: Localize(pStr: "Open the directory to add custom skins" )); |
| 489 | |
| 490 | TextRender()->SetFontPreset(EFontPreset::ICON_FONT); |
| 491 | TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); |
| 492 | static CButtonContainer s_SkinRefreshButton; |
| 493 | if(DoButton_Menu(pButtonContainer: &s_SkinRefreshButton, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: 0, pRect: &RefreshButton) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed())) |
| 494 | { |
| 495 | ShouldRefresh = true; |
| 496 | } |
| 497 | TextRender()->SetRenderFlags(0); |
| 498 | TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); |
| 499 | |
| 500 | if(ShouldRefresh) |
| 501 | { |
| 502 | GameClient()->RefreshSkins(SkinDescriptorFlags: CSkinDescriptor::FLAG_SIX); |
| 503 | } |
| 504 | } |
| 505 | |