UnleashedRecomp/UnleashedRecomp/ui/achievement_menu.cpp
Skyth (Asilkan) cdcacff53b
Implement SDF fonts. (#24)
* Initial SDF font generation work.

* Text now correctly displaying with proper spacing.

* Fix untextured draws, implement custom rectangles.

* Fix regular image display.

* Slightly refactor ImGui rendering.

* Implement outlines.

* Implement bevel.

* Create host device after loading the module if the installer wasn't run.

* Move ImGui files to its own folder.

* Fix outline sizes.

* Fix default ImGui font and font scales.

* Update font atlas files.
2024-12-11 23:30:19 +03:00

820 lines
27 KiB
C++

#include "achievement_menu.h"
#include "imgui_utils.h"
#include <api/SWA.h>
#include <gpu/video.h>
#include <kernel/xdbf.h>
#include <locale/locale.h>
#include <ui/button_guide.h>
#include <user/achievement_data.h>
#include <user/config.h>
#include <app.h>
#include <exports.h>
#include <decompressor.h>
#include <res/images/achievements_menu/trophy.dds.h>
#include <res/images/common/general_window.dds.h>
#include <res/images/common/select_fill.dds.h>
#include <gpu/imgui/imgui_snapshot.h>
constexpr double HEADER_CONTAINER_INTRO_MOTION_START = 0;
constexpr double HEADER_CONTAINER_INTRO_MOTION_END = 15;
constexpr double HEADER_CONTAINER_OUTRO_MOTION_START = 0;
constexpr double HEADER_CONTAINER_OUTRO_MOTION_END = 40;
constexpr double HEADER_CONTAINER_INTRO_FADE_START = 5;
constexpr double HEADER_CONTAINER_INTRO_FADE_END = 14;
constexpr double HEADER_CONTAINER_OUTRO_FADE_START = 0;
constexpr double HEADER_CONTAINER_OUTRO_FADE_END = 7;
constexpr double CONTENT_CONTAINER_COMMON_MOTION_START = 11;
constexpr double CONTENT_CONTAINER_COMMON_MOTION_END = 12;
constexpr double COUNTER_INTRO_FADE_START = 15;
constexpr double COUNTER_INTRO_FADE_END = 16;
constexpr double SELECTION_CONTAINER_BREATHE = 30;
static bool g_isClosing = false;
static double g_appearTime;
static std::vector<std::tuple<Achievement, time_t>> g_achievements;
static ImFont* g_fntSeurat;
static ImFont* g_fntNewRodinDB;
static ImFont* g_fntNewRodinUB;
static std::unique_ptr<GuestTexture> g_upTrophyIcon;
static std::unique_ptr<GuestTexture> g_upSelectionCursor;
static std::unique_ptr<GuestTexture> g_upWindow;
static int g_firstVisibleRowIndex;
static int g_selectedRowIndex;
static double g_rowSelectionTime;
static bool g_upWasHeld;
static bool g_downWasHeld;
static bool g_leftWasHeld;
static bool g_rightWasHeld;
static bool g_upRSWasHeld;
static bool g_downRSWasHeld;
static void ResetSelection()
{
g_firstVisibleRowIndex = 0;
g_selectedRowIndex = 0;
g_rowSelectionTime = ImGui::GetTime();
g_upWasHeld = false;
g_downWasHeld = false;
}
static void DrawContainer(ImVec2 min, ImVec2 max, ImU32 gradientTop, ImU32 gradientBottom, float alpha = 1, float cornerRadius = 25)
{
auto drawList = ImGui::GetForegroundDrawList();
DrawPauseContainer(g_upWindow.get(), min, max, alpha);
drawList->PushClipRect({ min.x, min.y + Scale(20) }, { max.x, max.y - Scale(5) });
}
static void DrawSelectionContainer(ImVec2 min, ImVec2 max)
{
auto drawList = ImGui::GetForegroundDrawList();
static auto breatheStart = ImGui::GetTime();
auto alpha = Lerp(1.0f, 0.75f, (sin((ImGui::GetTime() - breatheStart) * (2.0f * M_PI / (55.0f / 60.0f))) + 1.0f) / 2.0f);
auto colour = IM_COL32(255, 255, 255, 255 * alpha);
auto commonWidth = Scale(11);
auto commonHeight = Scale(24);
auto tl = PIXELS_TO_UV_COORDS(64, 64, 0, 0, 11, 24);
auto tc = PIXELS_TO_UV_COORDS(64, 64, 11, 0, 8, 24);
auto tr = PIXELS_TO_UV_COORDS(64, 64, 19, 0, 11, 24);
auto cl = PIXELS_TO_UV_COORDS(64, 64, 0, 24, 11, 2);
auto cc = PIXELS_TO_UV_COORDS(64, 64, 11, 24, 8, 2);
auto cr = PIXELS_TO_UV_COORDS(64, 64, 19, 24, 11, 2);
auto bl = PIXELS_TO_UV_COORDS(64, 64, 0, 26, 11, 24);
auto bc = PIXELS_TO_UV_COORDS(64, 64, 11, 26, 8, 24);
auto br = PIXELS_TO_UV_COORDS(64, 64, 19, 26, 11, 24);
drawList->AddImage(g_upSelectionCursor.get(), min, { min.x + commonWidth, min.y + commonHeight }, GET_UV_COORDS(tl), colour);
drawList->AddImage(g_upSelectionCursor.get(), { min.x + commonWidth, min.y }, { max.x - commonWidth, min.y + commonHeight }, GET_UV_COORDS(tc), colour);
drawList->AddImage(g_upSelectionCursor.get(), { max.x - commonWidth, min.y }, { max.x, min.y + commonHeight }, GET_UV_COORDS(tr), colour);
drawList->AddImage(g_upSelectionCursor.get(), { min.x, min.y + commonHeight }, { min.x + commonWidth, max.y - commonHeight }, GET_UV_COORDS(cl), colour);
drawList->AddImage(g_upSelectionCursor.get(), { min.x + commonWidth, min.y + commonHeight }, { max.x - commonWidth, max.y - commonHeight }, GET_UV_COORDS(cc), colour);
drawList->AddImage(g_upSelectionCursor.get(), { max.x - commonWidth, min.y + commonHeight }, { max.x, max.y - commonHeight }, GET_UV_COORDS(cr), colour);
drawList->AddImage(g_upSelectionCursor.get(), { min.x, max.y - commonHeight }, { min.x + commonWidth, max.y }, GET_UV_COORDS(bl), colour);
drawList->AddImage(g_upSelectionCursor.get(), { min.x + commonWidth, max.y - commonHeight }, { max.x - commonWidth, max.y }, GET_UV_COORDS(bc), colour);
drawList->AddImage(g_upSelectionCursor.get(), { max.x - commonWidth, max.y - commonHeight }, { max.x, max.y }, GET_UV_COORDS(br), colour);
}
static void DrawHeaderContainer(const char* text)
{
auto drawList = ImGui::GetForegroundDrawList();
auto fontSize = Scale(24);
auto textSize = g_fntNewRodinUB->CalcTextSizeA(fontSize, FLT_MAX, 0, text);
auto cornerRadius = 23;
auto textMarginX = Scale(16) + (Scale(cornerRadius) / 2);
auto containerMotion = g_isClosing
? ComputeMotion(g_appearTime, HEADER_CONTAINER_OUTRO_MOTION_START, HEADER_CONTAINER_OUTRO_MOTION_END)
: ComputeMotion(g_appearTime, HEADER_CONTAINER_INTRO_MOTION_START, HEADER_CONTAINER_INTRO_MOTION_END);
auto colourMotion = g_isClosing
? ComputeMotion(g_appearTime, HEADER_CONTAINER_OUTRO_FADE_START, HEADER_CONTAINER_OUTRO_FADE_END)
: ComputeMotion(g_appearTime, HEADER_CONTAINER_INTRO_FADE_START, HEADER_CONTAINER_INTRO_FADE_END);
// Slide animation.
auto containerMarginX = g_isClosing
? Hermite(251, 151, containerMotion)
: Hermite(151, 251, containerMotion);
// Transparency fade animation.
auto alpha = g_isClosing
? Lerp(1, 0, colourMotion)
: Lerp(0, 1, colourMotion);
ImVec2 min = { Scale(containerMarginX), Scale(136) };
ImVec2 max = { min.x + textMarginX * 2 + textSize.x + Scale(5), Scale(196) };
DrawPauseHeaderContainer(g_upWindow.get(), min, max, alpha);
SetTextSkew((min.y + max.y) / 2.0f, Scale(3.0f));
// TODO: Apply bevel.
DrawTextWithOutline
(
g_fntNewRodinUB,
fontSize,
{ /* X */ min.x + textMarginX, /* Y */ CENTRE_TEXT_VERT(min, max, textSize) - Scale(5) },
IM_COL32(255, 255, 255, 255 * alpha),
text,
4,
IM_COL32(0, 0, 0, 255 * alpha)
);
ResetTextSkew();
}
static void DrawAchievement(int rowIndex, float yOffset, Achievement& achievement, bool isUnlocked)
{
auto drawList = ImGui::GetForegroundDrawList();
auto clipRectMin = drawList->GetClipRectMin();
auto clipRectMax = drawList->GetClipRectMax();
auto itemWidth = Scale(700);
auto itemHeight = Scale(94);
auto itemMarginX = Scale(18);
auto imageMarginX = Scale(25);
auto imageMarginY = Scale(18);
auto imageSize = Scale(60);
ImVec2 min = { itemMarginX + clipRectMin.x, clipRectMin.y + itemHeight * rowIndex + yOffset };
ImVec2 max = { itemMarginX + min.x + itemWidth, min.y + itemHeight };
auto icon = g_xdbfTextureCache[achievement.ID];
auto isSelected = rowIndex == g_selectedRowIndex;
if (isSelected)
DrawSelectionContainer(min, max);
auto desc = isUnlocked ? achievement.UnlockedDesc.c_str() : achievement.LockedDesc.c_str();
auto fontSize = Scale(24);
auto textSize = g_fntSeurat->CalcTextSizeA(fontSize, FLT_MAX, 0, desc);
auto textX = min.x + imageMarginX + imageSize + itemMarginX * 2;
auto textMarqueeX = min.x + imageMarginX + imageSize;
auto titleTextY = Scale(20);
auto descTextY = Scale(52);
if (!isUnlocked)
SetShaderModifier(IMGUI_SHADER_MODIFIER_GRAYSCALE);
// Draw achievement icon.
drawList->AddImage
(
icon,
{ /* X */ min.x + imageMarginX, /* Y */ min.y + imageMarginY },
{ /* X */ min.x + imageMarginX + imageSize, /* Y */ min.y + imageMarginY + imageSize },
{ /* U */ 0, /* V */ 0 },
{ /* U */ 1, /* V */ 1 },
IM_COL32(255, 255, 255, 255 * (isUnlocked ? 1 : 0.5f))
);
if (!isUnlocked)
SetShaderModifier(IMGUI_SHADER_MODIFIER_NONE);
drawList->PushClipRect(min, max, true);
auto colLockedText = IM_COL32(60, 60, 60, 29);
auto colTextShadow = isUnlocked
? IM_COL32(0, 0, 0, 255)
: IM_COL32(60, 60, 60, 28);
auto shadowOffset = isUnlocked ? 2 : 1;
// Draw achievement name.
DrawTextWithShadow
(
g_fntSeurat,
fontSize,
{ textX, min.y + titleTextY },
isUnlocked ? IM_COL32(252, 243, 5, 255) : colLockedText,
achievement.Name.c_str(),
shadowOffset,
1.0f,
colTextShadow
);
ImVec2 marqueeMin = { textMarqueeX, min.y };
SetMarqueeFade(marqueeMin, max, Scale(32));
if (isSelected && textX + textSize.x >= max.x - Scale(10))
{
// Draw achievement description with marquee.
DrawTextWithMarqueeShadow
(
g_fntSeurat,
fontSize,
{ textX, min.y + descTextY },
marqueeMin,
max,
isUnlocked ? IM_COL32_WHITE : colLockedText,
desc,
g_rowSelectionTime,
0.9,
Scale(250),
shadowOffset,
0.4f,
colTextShadow
);
}
else
{
// Draw achievement description.
DrawTextWithShadow
(
g_fntSeurat,
fontSize,
{ textX, min.y + descTextY },
isUnlocked ? IM_COL32_WHITE : colLockedText,
desc,
shadowOffset,
1.0f,
colTextShadow
);
}
ResetMarqueeFade();
drawList->PopClipRect();
if (!isUnlocked)
return;
auto timestamp = AchievementData::GetTimestamp(achievement.ID);
if (!timestamp)
return;
char buffer[32];
struct tm time;
localtime_s(&time, &timestamp);
strftime(buffer, sizeof(buffer), "%Y/%m/%d %H:%M", &time);
fontSize = Scale(12);
textSize = g_fntNewRodinDB->CalcTextSizeA(fontSize, FLT_MAX, 0, buffer);
auto containerMarginX = Scale(10);
auto textMarginX = Scale(8);
ImVec2 timestampMin = { max.x - containerMarginX - textSize.x - (textMarginX * 2), min.y + titleTextY };
ImVec2 timestampMax = { max.x - containerMarginX, min.y + Scale(46) };
drawList->PushClipRect(min, max, true);
auto bevelOffset = Scale(6);
// Left
drawList->AddRectFilledMultiColor
(
timestampMin,
{ timestampMin.x + bevelOffset, timestampMax.y },
IM_COL32(255, 255, 255, 255),
IM_COL32(149, 149, 149, 40),
IM_COL32(149, 149, 149, 40),
IM_COL32(255, 255, 255, 255)
);
// Right
drawList->AddRectFilledMultiColor
(
{ timestampMax.x - bevelOffset, timestampMin.y },
{ timestampMax.x, timestampMax.y },
IM_COL32(149, 149, 149, 40),
IM_COL32(255, 255, 255, 255),
IM_COL32(255, 255, 255, 255),
IM_COL32(149, 149, 149, 40)
);
// Centre
drawList->AddRectFilled
(
{ timestampMin.x, timestampMin.y + bevelOffset },
{ timestampMax.x, timestampMax.y - bevelOffset },
IM_COL32(38, 38, 38, 172)
);
// Top
drawList->AddRectFilledMultiColor
(
timestampMin,
{ timestampMax.x, timestampMin.y + bevelOffset },
IM_COL32(16, 16, 16, 192),
IM_COL32(16, 16, 16, 192),
IM_COL32(38, 38, 38, 172),
IM_COL32(38, 38, 38, 172)
);
// Bottom
drawList->AddRectFilledMultiColor
(
{ timestampMin.x, timestampMax.y - bevelOffset },
{ timestampMax.x, timestampMax.y },
IM_COL32(38, 40, 38, 169),
IM_COL32(38, 40, 38, 169),
IM_COL32(16, 16, 16, 192),
IM_COL32(16, 16, 16, 192)
);
// Draw timestamp text.
DrawTextWithOutline
(
g_fntNewRodinDB,
fontSize,
{ /* X */ CENTRE_TEXT_HORZ(timestampMin, timestampMax, textSize), /* Y */ CENTRE_TEXT_VERT(timestampMin, timestampMax, textSize) },
IM_COL32(255, 255, 255, 255),
buffer,
4,
IM_COL32(8, 8, 8, 255)
);
drawList->PopClipRect();
}
static void DrawTrophySparkles(ImVec2 min, ImVec2 max, int recordCount, int trophyFrameIndex)
{
auto drawList = ImGui::GetForegroundDrawList();
constexpr auto recordsHalfTotal = ACH_RECORDS / 2;
// Don't sparkle the bronze trophy.
if (recordCount < recordsHalfTotal)
return;
static int trophyAnimCycles = 0;
static bool isIncrementedCycles = false;
bool isGoldTrophy = recordCount >= ACH_RECORDS;
if (!trophyFrameIndex && !isIncrementedCycles)
{
trophyAnimCycles++;
trophyAnimCycles %= isGoldTrophy ? 4 : 3;
isIncrementedCycles = true;
}
if (trophyFrameIndex >= 1)
isIncrementedCycles = false;
if (trophyAnimCycles >= 2)
{
auto marginX = Scale(9);
auto uv = PIXELS_TO_UV_COORDS(2048, 1024, 1984, 960, 64, 64);
auto& uv0 = std::get<0>(uv);
auto& uv1 = std::get<1>(uv);
auto colour = IM_COL32(240, 240, 200, 200);
static auto scaleStart = ImGui::GetTime();
auto scale = Scale(18) * Hermite(1.0f, 0.0f, (sin((ImGui::GetTime() - scaleStart) * (2.0f * M_PI / (15.0f / 60.0f))) + 1.0f) / 2.0f);
// Don't do extra sparkles for the silver trophy.
if (isGoldTrophy)
{
if (trophyFrameIndex >= 0 && trophyFrameIndex <= 5)
{
auto marginXAdd = Scale(1);
// Centre Left
drawList->AddImage
(
g_upTrophyIcon.get(),
{ min.x - scale / 2 + marginX + marginXAdd, max.y - ((max.y - min.y) / 2) - scale / 2 },
{ min.x + scale / 2 + marginX + marginXAdd, max.y - ((max.y - min.y) / 2) + scale / 2 },
uv0, uv1,
colour
);
}
if (trophyFrameIndex >= 16 && trophyFrameIndex <= 21)
{
auto marginXAdd = Scale(4);
auto marginY = Scale(11);
// Bottom Right
drawList->AddImage
(
g_upTrophyIcon.get(),
{ max.x - scale / 2 - (marginX + marginXAdd), max.y - scale / 2 - marginY },
{ max.x + scale / 2 - (marginX + marginXAdd), max.y + scale / 2 - marginY },
uv0, uv1,
colour
);
}
}
if (trophyFrameIndex >= 24 && trophyFrameIndex <= 29)
{
auto marginY = Scale(1);
// Top Right
drawList->AddImage
(
g_upTrophyIcon.get(),
{ max.x - scale / 2 - marginX, min.y - scale / 2 + marginY },
{ max.x + scale / 2 - marginX, min.y + scale / 2 + marginY },
uv0, uv1,
colour
);
}
}
}
static void DrawAchievementTotal(ImVec2 min, ImVec2 max)
{
auto drawList = ImGui::GetForegroundDrawList();
// Transparency fade animation.
auto alpha = Cubic(0, 1, ComputeMotion(g_appearTime, COUNTER_INTRO_FADE_START, COUNTER_INTRO_FADE_END));
auto imageMarginX = Scale(5);
auto imageMarginY = Scale(5);
auto imageSize = Scale(45);
ImVec2 imageMin = { max.x - imageSize - imageMarginX, min.y - imageSize - imageMarginY };
ImVec2 imageMax = { imageMin.x + imageSize, imageMin.y + imageSize };
constexpr auto columns = 8;
constexpr auto rows = 4;
constexpr auto spriteSize = 256.0f;
constexpr auto textureWidth = 2048.0f;
constexpr auto textureHeight = 1024.0f;
auto frameIndex = int32_t(floor(ImGui::GetTime() * 30.0f)) % 30;
auto columnIndex = frameIndex % columns;
auto rowIndex = frameIndex / columns;
auto uv0 = ImVec2(columnIndex * spriteSize / textureWidth, rowIndex * spriteSize / textureHeight);
auto uv1 = ImVec2((columnIndex + 1) * spriteSize / textureWidth, (rowIndex + 1) * spriteSize / textureHeight);
constexpr auto recordsHalfTotal = ACH_RECORDS / 2;
auto records = AchievementData::GetTotalRecords();
ImVec4 colBronze = ImGui::ColorConvertU32ToFloat4(IM_COL32(198, 105, 15, 255 * alpha));
ImVec4 colSilver = ImGui::ColorConvertU32ToFloat4(IM_COL32(220, 220, 220, 255 * alpha));
ImVec4 colGold = ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 195, 56, 255 * alpha));
ImVec4 colResult;
if (records <= 25)
{
float t = (float)records / 25.0f;
// Fade from bronze to silver.
colResult.x = colBronze.x + t * (colSilver.x - colBronze.x);
colResult.y = colBronze.y + t * (colSilver.y - colBronze.y);
colResult.z = colBronze.z + t * (colSilver.z - colBronze.z);
colResult.w = colBronze.w + t * (colSilver.w - colBronze.w);
}
else if (records <= 50)
{
float t = ((float)records - 25.0f) / 25.0f;
// Fade from silver to gold.
colResult.x = colSilver.x + t * (colGold.x - colSilver.x);
colResult.y = colSilver.y + t * (colGold.y - colSilver.y);
colResult.z = colSilver.z + t * (colGold.z - colSilver.z);
colResult.w = colSilver.w + t * (colGold.w - colSilver.w);
}
else
{
colResult = colGold;
}
drawList->AddImage(g_upTrophyIcon.get(), imageMin, imageMax, uv0, uv1, ImGui::ColorConvertFloat4ToU32(colResult));
// Add extra luminance to the trophy for bronze and gold.
if (records < recordsHalfTotal || records >= ACH_RECORDS)
drawList->AddImage(g_upTrophyIcon.get(), imageMin, imageMax, uv0, uv1, IM_COL32(255, 255, 255, 12));
// Draw sparkles on the trophy for silver and gold.
if (records >= recordsHalfTotal || records >= ACH_RECORDS)
DrawTrophySparkles(imageMin, imageMax, records, frameIndex);
auto str = std::format("{} / {}", records, ACH_RECORDS);
auto fontSize = Scale(20);
auto textSize = g_fntNewRodinDB->CalcTextSizeA(fontSize, FLT_MAX, 0, str.c_str());
DrawTextWithOutline
(
g_fntNewRodinDB,
fontSize,
{ /* X */ imageMin.x - textSize.x - Scale(6), /* Y */ CENTRE_TEXT_VERT(imageMin, imageMax, textSize) },
IM_COL32(255, 255, 255, 255 * alpha),
str.c_str(),
4,
IM_COL32(0, 0, 0, 255 * alpha)
);
}
static void DrawContentContainer()
{
auto drawList = ImGui::GetForegroundDrawList();
// Expand/retract animation.
auto motion = g_isClosing
? ComputeMotion(g_appearTime, 0, CONTENT_CONTAINER_COMMON_MOTION_START)
: ComputeMotion(g_appearTime, CONTENT_CONTAINER_COMMON_MOTION_START, CONTENT_CONTAINER_COMMON_MOTION_END);
auto minX = g_isClosing
? Hermite(251, 301, motion)
: Hermite(301, 251, motion);
auto minY = g_isClosing
? Hermite(189, 206, motion)
: Hermite(206, 189, motion);
auto maxX = g_isClosing
? Hermite(1031, 978, motion)
: Hermite(978, 1031, motion);
auto maxY = g_isClosing
? Hermite(604, 573, motion)
: Hermite(573, 604, motion);
ImVec2 min = { Scale(minX), Scale(minY) };
ImVec2 max = { Scale(maxX), Scale(maxY) };
// Transparency fade animation.
auto alpha = g_isClosing
? Hermite(1, 0, motion)
: Hermite(0, 1, motion);
DrawContainer(min, max, IM_COL32(197, 194, 197, 200), IM_COL32(115, 113, 115, 236), alpha);
if (motion < 1.0f)
{
return;
}
else if (g_isClosing)
{
AchievementMenu::s_isVisible = false;
return;
}
auto clipRectMin = drawList->GetClipRectMin();
auto clipRectMax = drawList->GetClipRectMax();
auto itemHeight = Scale(94);
auto yOffset = -g_firstVisibleRowIndex * itemHeight + Scale(2);
auto rowCount = 0;
// Draw separators.
for (int i = 1; i <= 3; i++)
{
auto lineMarginLeft = Scale(35);
auto lineMarginRight = Scale(55);
auto lineMarginY = Scale(2);
ImVec2 lineMin = { clipRectMin.x + lineMarginLeft, clipRectMin.y + itemHeight * i + lineMarginY };
ImVec2 lineMax = { clipRectMax.x - lineMarginRight, clipRectMin.y + itemHeight * i + lineMarginY };
drawList->AddLine(lineMin, lineMax, IM_COL32(163, 163, 163, 255));
drawList->AddLine({ lineMin.x, lineMin.y + Scale(1) }, { lineMax.x, lineMax.y + Scale(1) }, IM_COL32(143, 148, 143, 255));
}
for (auto& tpl : g_achievements)
{
auto achievement = std::get<0>(tpl);
if (AchievementData::IsUnlocked(achievement.ID))
DrawAchievement(rowCount++, yOffset, achievement, true);
}
for (auto& tpl : g_achievements)
{
auto achievement = std::get<0>(tpl);
if (!AchievementData::IsUnlocked(achievement.ID))
DrawAchievement(rowCount++, yOffset, achievement, false);
}
auto inputState = SWA::CInputState::GetInstance();
bool upIsHeld = inputState->GetPadState().IsDown(SWA::eKeyState_DpadUp) ||
inputState->GetPadState().LeftStickVertical > 0.5f;
bool downIsHeld = inputState->GetPadState().IsDown(SWA::eKeyState_DpadDown) ||
inputState->GetPadState().LeftStickVertical < -0.5f;
bool leftIsHeld = inputState->GetPadState().IsDown(SWA::eKeyState_DpadLeft) ||
inputState->GetPadState().LeftStickHorizontal < -0.5f;
bool rightIsHeld = inputState->GetPadState().IsDown(SWA::eKeyState_DpadRight) ||
inputState->GetPadState().LeftStickHorizontal > 0.5f;
bool upRSIsHeld = inputState->GetPadState().RightStickVertical > 0.5f;
bool downRSIsHeld = inputState->GetPadState().RightStickVertical < -0.5f;
bool isReachedTop = g_selectedRowIndex == 0;
bool isReachedBottom = g_selectedRowIndex == rowCount - 1;
bool scrollUp = !g_upWasHeld && upIsHeld;
bool scrollDown = !g_downWasHeld && downIsHeld;
bool scrollPageUp = !g_leftWasHeld && leftIsHeld && !isReachedTop;
bool scrollPageDown = !g_rightWasHeld && rightIsHeld && !isReachedBottom;
bool jumpToTop = !g_upRSWasHeld && upRSIsHeld && !isReachedTop;
bool jumpToBottom = !g_downRSWasHeld && downRSIsHeld && !isReachedBottom;
int prevSelectedRowIndex = g_selectedRowIndex;
if (scrollUp)
{
--g_selectedRowIndex;
if (g_selectedRowIndex < 0)
g_selectedRowIndex = rowCount - 1;
}
else if (scrollDown)
{
++g_selectedRowIndex;
if (g_selectedRowIndex >= rowCount)
g_selectedRowIndex = 0;
}
else if (scrollPageUp)
{
g_selectedRowIndex -= 3;
if (g_selectedRowIndex < 0)
g_selectedRowIndex = 0;
}
else if (scrollPageDown)
{
g_selectedRowIndex += 3;
if (g_selectedRowIndex >= rowCount)
g_selectedRowIndex = rowCount - 1;
}
else if (jumpToTop)
{
g_selectedRowIndex = 0;
}
else if (jumpToBottom)
{
g_selectedRowIndex = rowCount - 1;
}
// lol
if (scrollUp || scrollDown || scrollPageUp || scrollPageDown || jumpToTop || jumpToBottom)
{
g_rowSelectionTime = ImGui::GetTime();
Game_PlaySound("sys_actstg_pausecursor");
}
g_upWasHeld = upIsHeld;
g_downWasHeld = downIsHeld;
g_leftWasHeld = leftIsHeld;
g_rightWasHeld = rightIsHeld;
g_upRSWasHeld = upRSIsHeld;
g_downRSWasHeld = downRSIsHeld;
int visibleRowCount = int(floor((clipRectMax.y - clipRectMin.y) / itemHeight));
if (g_firstVisibleRowIndex > g_selectedRowIndex)
g_firstVisibleRowIndex = g_selectedRowIndex;
if (g_firstVisibleRowIndex + visibleRowCount - 1 < g_selectedRowIndex)
g_firstVisibleRowIndex = std::max(0, g_selectedRowIndex - visibleRowCount + 1);
// Pop clip rect from DrawContentContainer
drawList->PopClipRect();
DrawAchievementTotal(min, max);
// Draw scroll bar
if (rowCount > visibleRowCount)
{
float cornerRadius = Scale(25);
float totalHeight = (clipRectMax.y - clipRectMin.y - cornerRadius) - Scale(5);
float heightRatio = float(visibleRowCount) / float(rowCount);
float offsetRatio = float(g_firstVisibleRowIndex) / float(rowCount);
float offsetX = clipRectMax.x - Scale(39);
float offsetY = offsetRatio * totalHeight + clipRectMin.y + Scale(4);
float maxY = max.y - cornerRadius - Scale(3);
float lineThickness = Scale(1);
float innerMarginX = Scale(2);
float outerMarginX = Scale(24);
// Outline
drawList->AddRect
(
{ /* X */ offsetX - lineThickness, /* Y */ clipRectMin.y - lineThickness },
{ /* X */ clipRectMax.x - outerMarginX + lineThickness, /* Y */ maxY + lineThickness },
IM_COL32(255, 255, 255, 155),
Scale(1)
);
// Background
drawList->AddRectFilledMultiColor
(
{ /* X */ offsetX, /* Y */ clipRectMin.y },
{ /* X */ clipRectMax.x - outerMarginX, /* Y */ maxY },
IM_COL32(123, 125, 123, 255),
IM_COL32(123, 125, 123, 255),
IM_COL32(97, 99, 97, 255),
IM_COL32(97, 99, 97, 255)
);
// Scroll Bar Outline
drawList->AddRectFilledMultiColor
(
{ /* X */ offsetX + innerMarginX, /* Y */ offsetY - lineThickness },
{ /* X */ clipRectMax.x - outerMarginX - innerMarginX, /* Y */ offsetY + lineThickness + totalHeight * heightRatio },
IM_COL32(185, 185, 185, 255),
IM_COL32(185, 185, 185, 255),
IM_COL32(172, 172, 172, 255),
IM_COL32(172, 172, 172, 255)
);
// Scroll Bar
drawList->AddRectFilled
(
{ /* X */ offsetX + innerMarginX + lineThickness, /* Y */ offsetY },
{ /* X */ clipRectMax.x - outerMarginX - innerMarginX - lineThickness, /* Y */ offsetY + totalHeight * heightRatio },
IM_COL32(255, 255, 255, 255)
);
}
}
void AchievementMenu::Init()
{
auto& io = ImGui::GetIO();
g_fntSeurat = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf");
g_fntNewRodinDB = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-DB.otf");
g_fntNewRodinUB = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-UB.otf");
g_upTrophyIcon = LOAD_ZSTD_TEXTURE(g_trophy);
g_upSelectionCursor = LOAD_ZSTD_TEXTURE(g_select_fill);
g_upWindow = LOAD_ZSTD_TEXTURE(g_general_window);
}
void AchievementMenu::Draw()
{
if (!s_isVisible)
return;
DrawHeaderContainer(Localise("Achievements_Name_Uppercase").c_str());
DrawContentContainer();
}
void AchievementMenu::Open()
{
s_isVisible = true;
g_isClosing = false;
g_appearTime = ImGui::GetTime();
g_achievements.clear();
for (auto& achievement : g_xdbfWrapper.GetAchievements((EXDBFLanguage)Config::Language.Value))
g_achievements.push_back(std::make_tuple(achievement, AchievementData::GetTimestamp(achievement.ID)));
std::sort(g_achievements.begin(), g_achievements.end(), [](const auto& a, const auto& b)
{
return std::get<1>(a) > std::get<1>(b);
});
ButtonGuide::Open(Button(Localise("Common_Back"), EButtonIcon::B));
ResetSelection();
Game_PlaySound("sys_actstg_pausewinopen");
}
void AchievementMenu::Close()
{
if (!g_isClosing)
{
g_appearTime = ImGui::GetTime();
g_isClosing = true;
}
ButtonGuide::Close();
Game_PlaySound("sys_actstg_pausewinclose");
Game_PlaySound("sys_actstg_pausecansel");
}