mirror of
https://github.com/hedge-dev/UnleashedRecomp.git
synced 2025-04-28 13:27:58 +03:00

* Initial Linux attempt. * Add clang toolchain & make tools compile. * vcpkg as submodule. * First implementation of IO rewrite. (#31) * Fix directory iteration resolving symlinks. * Refactor kernel objects to be lock-free. * Implement guest critical sections using std::atomic. * Make D3D12 support optional. (#33) * Make D3D12 support optional. * Update ShaderRecomp, fix macros. * Replace QueryPerformanceCounter. (#35) * Add Linux home path for GetUserPath(). (#36) * Cross-platform Sleep. (#37) * Add mmap implementations for virtual allocation. (#38) * Cross-platform TLS. (#34) * Cross-platform TLS. * Fix front() to back(), use Mutex. * Fix global variable namings. --------- Co-authored-by: Skyth <19259897+blueskythlikesclouds@users.noreply.github.com> * Unicode support. (#39) * Replace CreateDirectoryA with Unicode version. * Cross platform thread implementation. (#41) * Cross-platform thread implementation. * Put set thread name calls behind a Win32 macro. * Cross-platform semaphore implementation. (#43) * xam: use SDL for keyboard input * Cross-platform atomic operations. (#44) * Cross-platform spin lock implementation. * Cross-platform reference counting. * Cross-platform event implementation. (#47) * Compiling and running on Linux. (#49) * Current work trying to get it to compile. * Update vcpkg.json baseline. * vcpkg, memory mapped file. * Bitscan forward. * Fix localtime_s. * FPS patches high res clock. * Rename Window to GameWindow. Fix guest pointers. * GetCurrentThreadID gone. * Code cache pointers, RenderWindow type. * Add Linux stubs. * Refactor Config. * Fix paths. * Add linux-release config. * FS fixes. * Fix Windows compilation errors & unicode converter crash. * Rename physical memory allocation functions to not clash with X11. * Fix NULL character being added on RtlMultiByteToUnicodeN. * Use std::exit. * Add protection to memory on Linux. * Convert majority of dependencies to submodules. (#48) * Convert majority of dependencies to submodules. * Don't compile header-only libraries. * Fix a few incorrect data types. * Fix config directory. * Unicode fixes & sizeof asserts. * Change the exit function to not call static destructors. * Fix files picker. * Add RelWithDebInfo preset for Linux. * Implement OS Restart on Linux. (#50) --------- Co-authored-by: Dario <dariosamo@gmail.com> * Update PowerRecomp. * Add Env Var detection for VCPKG_ROOT, add DLC detection. * Use error code version on DLC directory iterator. * Set D3D12MA::ALLOCATOR_FLAG_DONT_PREFER_SMALL_BUFFERS_COMMITTED flag. * Linux flatpak. (#51) * Add flatpak support. * Add game install directory override for flatpak. * Flatpak'ing. * Flatpak it some more. * We flat it, we pak it. * Flatpak'd. * The Marvelous Misadventures of Flatpak. * Attempt to change logic of NFD and show error. * Flattenpakken. * Use game install directory instead of current path. * Attempt to fix line endings. * Update io.github.hedge_dev.unleashedrecomp.json * Fix system time query implementation. * Add Present Wait to Vulkan to improve frame pacing and reduce latency. (#53) * Add present wait support to Vulkan. * Default to triple buffering if presentWait is supported. * Bracey fellas. * Update paths.h * SDL2 audio (again). (#52) * Implement SDL2 audio (again). * Call timeBeginPeriod/timeEndPeriod. * Replace miniaudio with SDL mixer. * Queue audio samples in a separate thread. * Enable CMake option override policy & fix compilation error. * Fix compilation error on Linux. * Fix but also trim shared strings. * Wayland support. (#55) * Make channel index a global variable in embedded player. * Fix SDL Audio selection for OGG on Flatpak. * Minor installer wizard fixes. * Fix compilation error. * Yield in model consumer and pipeline compiler threads. * Special case Sleep(0) to yield on Linux. * Add App Id hint. * Correct implementation for auto reset events. (#57) --------- Co-authored-by: Dario <dariosamo@gmail.com> Co-authored-by: Hyper <34012267+hyperbx@users.noreply.github.com>
832 lines
27 KiB
C++
832 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(80, 80, 80, 127);
|
|
|
|
auto colTextShadow = isUnlocked
|
|
? IM_COL32(0, 0, 0, 255)
|
|
: IM_COL32(20, 20, 20, 127);
|
|
|
|
auto shadowOffset = isUnlocked ? 2 : 1;
|
|
auto shadowRadius = isUnlocked ? 1 : 0.5f;
|
|
|
|
// Draw achievement name.
|
|
DrawTextWithShadow
|
|
(
|
|
g_fntSeurat,
|
|
fontSize,
|
|
{ textX, min.y + titleTextY },
|
|
isUnlocked ? IM_COL32(252, 243, 5, 255) : colLockedText,
|
|
achievement.Name.c_str(),
|
|
shadowOffset,
|
|
shadowRadius,
|
|
colTextShadow
|
|
);
|
|
|
|
ImVec2 marqueeMin = { textMarqueeX, min.y };
|
|
ImVec2 marqueeMax = { max.x - Scale(10) /* timestamp margin X */, max.y };
|
|
|
|
SetMarqueeFade(marqueeMin, marqueeMax, 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,
|
|
marqueeMax,
|
|
isUnlocked ? IM_COL32_WHITE : colLockedText,
|
|
desc,
|
|
g_rowSelectionTime,
|
|
0.9,
|
|
Scale(250),
|
|
shadowOffset,
|
|
shadowRadius,
|
|
colTextShadow
|
|
);
|
|
}
|
|
else
|
|
{
|
|
// Draw achievement description.
|
|
DrawTextWithShadow
|
|
(
|
|
g_fntSeurat,
|
|
fontSize,
|
|
{ textX, min.y + descTextY },
|
|
isUnlocked ? IM_COL32_WHITE : colLockedText,
|
|
desc,
|
|
shadowOffset,
|
|
shadowRadius,
|
|
colTextShadow
|
|
);
|
|
}
|
|
|
|
ResetMarqueeFade();
|
|
|
|
drawList->PopClipRect();
|
|
|
|
if (!isUnlocked)
|
|
return;
|
|
|
|
auto timestamp = AchievementData::GetTimestamp(achievement.ID);
|
|
|
|
if (!timestamp)
|
|
return;
|
|
|
|
char buffer[32];
|
|
#ifdef _WIN32
|
|
tm timeStruct;
|
|
tm *timePtr = &timeStruct;
|
|
localtime_s(timePtr, ×tamp);
|
|
#else
|
|
tm *timePtr = localtime(×tamp);
|
|
#endif
|
|
strftime(buffer, sizeof(buffer), "%Y/%m/%d %H:%M", timePtr);
|
|
|
|
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 = fmt::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))
|
|
{
|
|
if (Config::Language == ELanguage::English)
|
|
achievement.Name = xdbf::FixInvalidSequences(achievement.Name);
|
|
|
|
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");
|
|
}
|