forked from tsuki/openroller
Reproduce arcade menu transitions
This commit is contained in:
+434
-60
@@ -17,6 +17,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <unordered_map>
|
||||
@@ -45,6 +46,67 @@ enum class SelectTask {
|
||||
Difficulty,
|
||||
};
|
||||
|
||||
enum class MenuMotion {
|
||||
MusicEnter,
|
||||
Idle,
|
||||
SongScroll,
|
||||
SortChange,
|
||||
MusicDecision,
|
||||
MusicExit,
|
||||
ModeEnter,
|
||||
DifficultyChange,
|
||||
ModeExitBack,
|
||||
ModeDecision,
|
||||
ModeExitConfirm,
|
||||
};
|
||||
|
||||
constexpr float kMenuEnterSeconds = 0.375f;
|
||||
constexpr float kMenuExitSeconds = 0.375f;
|
||||
constexpr float kSongScrollSeconds = 0.125f;
|
||||
constexpr float kMenuChangeSeconds = 0.15f;
|
||||
constexpr float kDecisionSeconds = 0.25f;
|
||||
|
||||
constexpr std::array<int, 12> kCarouselOffsets{
|
||||
-5, -4, -3, -2, -1, 0, 0, 1, 2, 3, 4, 5
|
||||
};
|
||||
constexpr std::array<float, 12> kCarouselRowY{
|
||||
175, 228, 281, 334, 387, 440, 701, 754, 807, 860, 913, 966
|
||||
};
|
||||
constexpr std::array<float, 12> kCarouselIndexY{
|
||||
189, 242, 295, 348, 401, 454, 715, 768, 821, 874, 927, 980
|
||||
};
|
||||
constexpr std::array<float, 12> kCarouselCentersY{
|
||||
209, 262, 315, 368, 421, 474, 735, 788, 841, 894, 947, 1000
|
||||
};
|
||||
constexpr std::array<float, 12> kCarouselAlpha{
|
||||
0.0f, 0.7f, 0.8f, 0.9f, 1.0f, 0.0f,
|
||||
0.0f, 1.0f, 0.9f, 0.8f, 0.7f, 0.0f
|
||||
};
|
||||
|
||||
float saturate(float value) {
|
||||
return std::clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float lerp(float from, float to, float amount) {
|
||||
return from + (to - from) * amount;
|
||||
}
|
||||
|
||||
int carouselSlotForOffset(int offset, bool lowerZero = false) {
|
||||
if (offset < -5 || offset > 5) return -1;
|
||||
if (offset < 0) return offset + 5;
|
||||
if (offset > 0) return offset + 6;
|
||||
return lowerZero ? 6 : 5;
|
||||
}
|
||||
|
||||
int wrappedCarouselDistance(size_t origin, size_t target, size_t count) {
|
||||
if (count == 0) return 0;
|
||||
int distance = static_cast<int>(target) - static_cast<int>(origin);
|
||||
const int wrappedCount = static_cast<int>(count);
|
||||
if (distance > wrappedCount / 2) distance -= wrappedCount;
|
||||
if (distance < -wrappedCount / 2) distance += wrappedCount;
|
||||
return distance;
|
||||
}
|
||||
|
||||
struct Song {
|
||||
gc::StageCatalogEntry catalog;
|
||||
fs::path menuTexture;
|
||||
@@ -433,9 +495,16 @@ public:
|
||||
const auto found = textures_.find(draw.imageSymbol);
|
||||
if (found == textures_.end()) continue;
|
||||
auto corners = draw.corners;
|
||||
float symbolDx = 0.0f;
|
||||
float symbolDy = 0.0f;
|
||||
const auto translated = symbolTranslations_.find(draw.imageSymbol);
|
||||
if (translated != symbolTranslations_.end()) {
|
||||
symbolDx = translated->second[0];
|
||||
symbolDy = translated->second[1];
|
||||
}
|
||||
for (auto& corner : corners) {
|
||||
corner[0] += dx;
|
||||
corner[1] += dy;
|
||||
corner[0] += dx + symbolDx;
|
||||
corner[1] += dy + symbolDy;
|
||||
}
|
||||
ui.textureQuad(found->second, corners,
|
||||
glm::vec4(draw.color[0], draw.color[1], draw.color[2],
|
||||
@@ -446,13 +515,7 @@ public:
|
||||
bool ready() const { return !textures_.empty(); }
|
||||
|
||||
void translateSymbol(const std::string& symbol, float dx, float dy) {
|
||||
for (gc::RvbImageDraw& draw : draws_) {
|
||||
if (draw.imageSymbol != symbol) continue;
|
||||
for (auto& corner : draw.corners) {
|
||||
corner[0] += dx;
|
||||
corner[1] += dy;
|
||||
}
|
||||
}
|
||||
symbolTranslations_[symbol] = {dx, dy};
|
||||
}
|
||||
|
||||
void clear() {
|
||||
@@ -460,6 +523,7 @@ public:
|
||||
if (texture.id != 0) glDeleteTextures(1, &texture.id);
|
||||
}
|
||||
textures_.clear();
|
||||
symbolTranslations_.clear();
|
||||
draws_.clear();
|
||||
rvbBytes_.clear();
|
||||
mtxBytes_.clear();
|
||||
@@ -488,6 +552,7 @@ private:
|
||||
}
|
||||
std::vector<gc::RvbImageDraw> draws_;
|
||||
std::unordered_map<std::string, DdsTexture> textures_;
|
||||
std::unordered_map<std::string, std::array<float, 2>> symbolTranslations_;
|
||||
std::vector<uint8_t> rvbBytes_;
|
||||
std::vector<uint8_t> mtxBytes_;
|
||||
gc::RvbScene scene_;
|
||||
@@ -563,6 +628,22 @@ gc::RvbSnapshotState selectMusicSceneState(const Song* song = nullptr,
|
||||
return state;
|
||||
}
|
||||
|
||||
gc::RvbSnapshotState selectMusicExitSceneState(const Song* song, int activeSort) {
|
||||
gc::RvbSnapshotState state = selectMusicSceneState(song, activeSort);
|
||||
auto& frame = state.frameByPath;
|
||||
frame["/"] = "jf_slmusic_end";
|
||||
frame["/imc_navi"] = "tg_navi_end";
|
||||
frame["/imc_title"] = "lf_title_selectmusic_fo";
|
||||
frame["/imc_focus"] = "jf_focus_fd_out_m";
|
||||
return state;
|
||||
}
|
||||
|
||||
gc::RvbSnapshotState selectMusicDecisionSceneState(const Song* song, int activeSort) {
|
||||
gc::RvbSnapshotState state = selectMusicSceneState(song, activeSort);
|
||||
state.frameByPath["/imc_focus"] = "jf_decision";
|
||||
return state;
|
||||
}
|
||||
|
||||
gc::RvbSnapshotState selectModeSceneState(const Song* song = nullptr, int difficulty = 0) {
|
||||
gc::RvbSnapshotState state;
|
||||
auto& frame = state.frameByPath;
|
||||
@@ -604,6 +685,16 @@ gc::RvbSnapshotState selectModeSceneState(const Song* song = nullptr, int diffic
|
||||
return state;
|
||||
}
|
||||
|
||||
gc::RvbSnapshotState selectModeExitSceneState(const Song* song, int difficulty) {
|
||||
gc::RvbSnapshotState state = selectModeSceneState(song, difficulty);
|
||||
auto& frame = state.frameByPath;
|
||||
frame["/"] = "jf_slmode_end";
|
||||
frame["/imc_navi"] = "tg_navi_end";
|
||||
frame["/imc_title"] = "jf_title_mode_fo";
|
||||
frame["/imc_slmode"] = "jf_mode_fo";
|
||||
return state;
|
||||
}
|
||||
|
||||
gc::RvbSnapshotState commonSelectSceneState() {
|
||||
gc::RvbSnapshotState state;
|
||||
auto& frame = state.frameByPath;
|
||||
@@ -623,6 +714,15 @@ gc::RvbSnapshotState commonSelectSceneState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
gc::RvbSnapshotState commonSelectExitSceneState() {
|
||||
gc::RvbSnapshotState state = commonSelectSceneState();
|
||||
auto& frame = state.frameByPath;
|
||||
frame["/imc_ctrl_anim"] = "jf_ctrl_end";
|
||||
frame["/imc_head"] = "jf_head_fo";
|
||||
frame["/imc_foot"] = "jf_foot_fo";
|
||||
return state;
|
||||
}
|
||||
|
||||
gc::RvbSnapshotState navigatorSelectSceneState() {
|
||||
gc::RvbSnapshotState state;
|
||||
auto& frame = state.frameByPath;
|
||||
@@ -633,6 +733,21 @@ gc::RvbSnapshotState navigatorSelectSceneState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
gc::RvbSnapshotState navigatorSelectExitSceneState() {
|
||||
gc::RvbSnapshotState state = navigatorSelectSceneState();
|
||||
state.frameByPath["/"] = "jf_ope_end";
|
||||
return state;
|
||||
}
|
||||
|
||||
gc::RvbSnapshotState animateRvbState(
|
||||
gc::RvbSnapshotState state, uint32_t animationFrame,
|
||||
std::initializer_list<const char*> paths) {
|
||||
for (const char* path : paths) {
|
||||
state.animationFrameByPath[path] = animationFrame;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
int genreLabelRow(int genre) {
|
||||
// s_j[_eng].dds is the executable-owned 256x256 genre-label atlas.
|
||||
// Each pseudo song ID 50000+n selects one 256x32 row.
|
||||
@@ -874,6 +989,14 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
rebuildVisible();
|
||||
|
||||
size_t selection = 0;
|
||||
size_t previousSelection = selection;
|
||||
int scrollDelta = 0;
|
||||
MenuMotion motion = MenuMotion::MusicEnter;
|
||||
Uint64 motionStartTicks = SDL_GetTicks();
|
||||
const auto startMotion = [&](MenuMotion next) {
|
||||
motion = next;
|
||||
motionStartTicks = SDL_GetTicks();
|
||||
};
|
||||
int difficulty = firstPlayableDifficulty(songs[visible[selection]]);
|
||||
auto refreshOriginalScenes = [&] {
|
||||
if (visible.empty()) return;
|
||||
@@ -888,12 +1011,15 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
refreshOriginalScenes();
|
||||
auto changeSong = [&](int delta) {
|
||||
if (visible.empty()) return;
|
||||
previousSelection = selection;
|
||||
scrollDelta = delta;
|
||||
const int count = static_cast<int>(visible.size());
|
||||
int next = (static_cast<int>(selection) + delta) % count;
|
||||
if (next < 0) next += count;
|
||||
selection = static_cast<size_t>(next);
|
||||
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
|
||||
refreshOriginalScenes();
|
||||
startMotion(MenuMotion::SongScroll);
|
||||
};
|
||||
auto changeSort = [&](int delta) {
|
||||
const size_t selectedSong = visible.empty() ? 0 : visible[selection];
|
||||
@@ -908,6 +1034,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
|
||||
}
|
||||
refreshOriginalScenes();
|
||||
startMotion(MenuMotion::SortChange);
|
||||
};
|
||||
auto changeDifficulty = [&](int delta) {
|
||||
const Song& song = songs[visible[selection]];
|
||||
@@ -917,6 +1044,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
if (!song.stages[next].empty()) {
|
||||
difficulty = next;
|
||||
refreshOriginalScenes();
|
||||
startMotion(MenuMotion::DifficultyChange);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -927,10 +1055,78 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
SelectTask task = SelectTask::Music;
|
||||
bool capturedMusic = false;
|
||||
bool capturedDifficulty = false;
|
||||
bool capturedMusicDecision = false;
|
||||
bool capturedMusicExit = false;
|
||||
bool capturedModeEnter = false;
|
||||
const char* capturePrefix = std::getenv("OPENROLLER_CAPTURE_PREFIX");
|
||||
const bool captureBoth = std::getenv("OPENROLLER_CAPTURE_BOTH") != nullptr;
|
||||
const bool captureTransitions =
|
||||
std::getenv("OPENROLLER_CAPTURE_TRANSITIONS") != nullptr;
|
||||
const Uint64 menuStartTicks = SDL_GetTicks();
|
||||
while (running) {
|
||||
const Uint64 nowTicks = SDL_GetTicks();
|
||||
const float motionSeconds = static_cast<float>(nowTicks - motionStartTicks) / 1000.0f;
|
||||
switch (motion) {
|
||||
case MenuMotion::MusicEnter:
|
||||
if (motionSeconds >= kMenuEnterSeconds) {
|
||||
motion = MenuMotion::Idle;
|
||||
refreshOriginalScenes();
|
||||
originalCommon.setState(commonSelectSceneState());
|
||||
originalNavigator.setState(navigatorSelectSceneState());
|
||||
}
|
||||
break;
|
||||
case MenuMotion::SongScroll:
|
||||
if (motionSeconds >= kSongScrollSeconds) {
|
||||
motion = MenuMotion::Idle;
|
||||
refreshOriginalScenes();
|
||||
}
|
||||
break;
|
||||
case MenuMotion::SortChange:
|
||||
case MenuMotion::DifficultyChange:
|
||||
if (motionSeconds >= kMenuChangeSeconds) {
|
||||
motion = MenuMotion::Idle;
|
||||
refreshOriginalScenes();
|
||||
}
|
||||
break;
|
||||
case MenuMotion::MusicDecision:
|
||||
if (motionSeconds >= kDecisionSeconds) {
|
||||
startMotion(MenuMotion::MusicExit);
|
||||
}
|
||||
break;
|
||||
case MenuMotion::MusicExit:
|
||||
if (motionSeconds >= kMenuExitSeconds) {
|
||||
task = SelectTask::Difficulty;
|
||||
startMotion(MenuMotion::ModeEnter);
|
||||
}
|
||||
break;
|
||||
case MenuMotion::ModeEnter:
|
||||
if (motionSeconds >= kMenuEnterSeconds) {
|
||||
motion = MenuMotion::Idle;
|
||||
refreshOriginalScenes();
|
||||
originalCommon.setState(commonSelectSceneState());
|
||||
originalNavigator.setState(navigatorSelectSceneState());
|
||||
}
|
||||
break;
|
||||
case MenuMotion::ModeExitBack:
|
||||
if (motionSeconds >= kMenuExitSeconds) {
|
||||
task = SelectTask::Music;
|
||||
startMotion(MenuMotion::MusicEnter);
|
||||
}
|
||||
break;
|
||||
case MenuMotion::ModeDecision:
|
||||
if (motionSeconds >= kDecisionSeconds) {
|
||||
startMotion(MenuMotion::ModeExitConfirm);
|
||||
}
|
||||
break;
|
||||
case MenuMotion::ModeExitConfirm:
|
||||
if (motionSeconds >= kMenuExitSeconds) {
|
||||
confirmed = true;
|
||||
running = false;
|
||||
}
|
||||
break;
|
||||
case MenuMotion::Idle:
|
||||
break;
|
||||
}
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_EVENT_QUIT) running = false;
|
||||
@@ -940,6 +1136,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
SDL_SetWindowTitle(window, "OpenRoller");
|
||||
continue;
|
||||
}
|
||||
if (motion != MenuMotion::Idle) continue;
|
||||
if (task == SelectTask::Music) {
|
||||
switch (event.key.key) {
|
||||
case SDLK_ESCAPE: running = false; break;
|
||||
@@ -950,14 +1147,14 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
case SDLK_Q: changeSort(-1); break;
|
||||
case SDLK_E: case SDLK_TAB: changeSort(1); break;
|
||||
case SDLK_RETURN: case SDLK_SPACE:
|
||||
task = SelectTask::Difficulty;
|
||||
startMotion(MenuMotion::MusicDecision);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
} else {
|
||||
switch (event.key.key) {
|
||||
case SDLK_ESCAPE:
|
||||
task = SelectTask::Music;
|
||||
startMotion(MenuMotion::ModeExitBack);
|
||||
break;
|
||||
case SDLK_UP: case SDLK_W: case SDLK_LEFT: case SDLK_A:
|
||||
changeDifficulty(-1);
|
||||
@@ -966,8 +1163,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
changeDifficulty(1);
|
||||
break;
|
||||
case SDLK_RETURN: case SDLK_SPACE:
|
||||
confirmed = true;
|
||||
running = false;
|
||||
startMotion(MenuMotion::ModeDecision);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
@@ -976,6 +1172,86 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
if (visible.empty()) continue;
|
||||
|
||||
const Song& selected = songs[visible[selection]];
|
||||
const float activeMotionSeconds =
|
||||
static_cast<float>(SDL_GetTicks() - motionStartTicks) / 1000.0f;
|
||||
const uint32_t rvbFrame = static_cast<uint32_t>(activeMotionSeconds * 60.0f);
|
||||
if (motion == MenuMotion::MusicEnter) {
|
||||
originalMusic.setState(animateRvbState(
|
||||
selectMusicSceneState(&selected, static_cast<int>(sortSlot + 1)), rvbFrame,
|
||||
{"/", "/imc_navi", "/imc_title", "/imc_focus",
|
||||
"/imc_focus/imc_fd_jacket_anim", "/imc_sort"}));
|
||||
originalCommon.setState(animateRvbState(
|
||||
commonSelectSceneState(), rvbFrame,
|
||||
{"/imc_ctrl_anim", "/imc_head", "/imc_foot"}));
|
||||
originalNavigator.setState(animateRvbState(
|
||||
navigatorSelectSceneState(), rvbFrame, {"/", "/imc_ope"}));
|
||||
} else if (motion == MenuMotion::SongScroll) {
|
||||
gc::RvbSnapshotState state =
|
||||
selectMusicSceneState(&selected, static_cast<int>(sortSlot + 1));
|
||||
state.frameByPath["/imc_focus"] = "jf_focus_fd_move";
|
||||
originalMusic.setState(animateRvbState(
|
||||
std::move(state), rvbFrame, {"/imc_focus"}));
|
||||
} else if (motion == MenuMotion::SortChange) {
|
||||
originalMusic.setState(animateRvbState(
|
||||
selectMusicSceneState(&selected, static_cast<int>(sortSlot + 1)), rvbFrame,
|
||||
{"/imc_sort"}));
|
||||
} else if (motion == MenuMotion::MusicDecision) {
|
||||
originalMusic.setState(animateRvbState(
|
||||
selectMusicDecisionSceneState(&selected, static_cast<int>(sortSlot + 1)),
|
||||
rvbFrame, {"/imc_focus"}));
|
||||
} else if (motion == MenuMotion::MusicExit) {
|
||||
originalMusic.setState(animateRvbState(
|
||||
selectMusicExitSceneState(&selected, static_cast<int>(sortSlot + 1)), rvbFrame,
|
||||
{"/", "/imc_navi", "/imc_title", "/imc_focus"}));
|
||||
originalCommon.setState(animateRvbState(
|
||||
commonSelectExitSceneState(), rvbFrame,
|
||||
{"/imc_ctrl_anim", "/imc_head", "/imc_foot"}));
|
||||
originalNavigator.setState(animateRvbState(
|
||||
navigatorSelectExitSceneState(), rvbFrame, {"/"}));
|
||||
} else if (motion == MenuMotion::ModeEnter) {
|
||||
originalDifficulty.setState(animateRvbState(
|
||||
selectModeSceneState(&selected, difficulty), rvbFrame,
|
||||
{"/", "/imc_navi", "/imc_title", "/imc_slmode", "/imc_tab"}));
|
||||
originalCommon.setState(animateRvbState(
|
||||
commonSelectSceneState(), rvbFrame,
|
||||
{"/imc_ctrl_anim", "/imc_head", "/imc_foot"}));
|
||||
originalNavigator.setState(animateRvbState(
|
||||
navigatorSelectSceneState(), rvbFrame, {"/", "/imc_ope"}));
|
||||
} else if (motion == MenuMotion::DifficultyChange) {
|
||||
originalDifficulty.setState(animateRvbState(
|
||||
selectModeSceneState(&selected, difficulty), rvbFrame,
|
||||
{"/imc_slmode/imc_mode/imc_m_smpl",
|
||||
"/imc_slmode/imc_mode/imc_m_nrml",
|
||||
"/imc_slmode/imc_mode/imc_m_hard",
|
||||
"/imc_slmode/imc_mode/imc_m_extra",
|
||||
"/imc_slmode/imc_mode/imc_tri_set_exoff",
|
||||
"/imc_slmode/imc_mode/imc_tri_set_exon",
|
||||
"/imc_tab/imc_info_m_tab"}));
|
||||
} else if (motion == MenuMotion::ModeDecision) {
|
||||
gc::RvbSnapshotState state = selectModeSceneState(&selected, difficulty);
|
||||
static constexpr std::array<const char*, 4> focusPaths{
|
||||
"/imc_slmode/imc_mode/imc_m_smpl/imc_m_focus",
|
||||
"/imc_slmode/imc_mode/imc_m_nrml/imc_m_focus",
|
||||
"/imc_slmode/imc_mode/imc_m_hard/imc_m_focus",
|
||||
"/imc_slmode/imc_mode/imc_m_extra/imc_m_focus"
|
||||
};
|
||||
const char* focusPath = focusPaths[static_cast<size_t>(difficulty)];
|
||||
state.frameByPath[focusPath] = "jf_mode_decision";
|
||||
originalDifficulty.setState(animateRvbState(
|
||||
std::move(state), rvbFrame, {focusPath}));
|
||||
} else if (motion == MenuMotion::ModeExitBack ||
|
||||
motion == MenuMotion::ModeExitConfirm) {
|
||||
gc::RvbSnapshotState state =
|
||||
selectModeExitSceneState(&selected, difficulty);
|
||||
originalDifficulty.setState(animateRvbState(
|
||||
std::move(state), rvbFrame,
|
||||
{"/", "/imc_navi", "/imc_title", "/imc_slmode"}));
|
||||
originalCommon.setState(animateRvbState(
|
||||
commonSelectExitSceneState(), rvbFrame,
|
||||
{"/imc_ctrl_anim", "/imc_head", "/imc_foot"}));
|
||||
originalNavigator.setState(animateRvbState(
|
||||
navigatorSelectExitSceneState(), rvbFrame, {"/"}));
|
||||
}
|
||||
SDL_SetWindowTitle(window, ("OpenRoller - " + selected.catalog.imageKey).c_str());
|
||||
int pixelWidth = 0, pixelHeight = 0;
|
||||
SDL_GetWindowSizeInPixels(window, &pixelWidth, &pixelHeight);
|
||||
@@ -1020,39 +1296,90 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
if (logical < 0) logical += count;
|
||||
return carousel[static_cast<size_t>(logical)];
|
||||
};
|
||||
size_t previousCarouselEntry = selectedCarouselEntry;
|
||||
if (motion == MenuMotion::SongScroll && previousSelection < visible.size()) {
|
||||
const size_t previousSongIndex = visible[previousSelection];
|
||||
const auto previousEntry = std::find_if(
|
||||
carousel.begin(), carousel.end(), [previousSongIndex](const CarouselEntry& entry) {
|
||||
return !entry.category && entry.songIndex == previousSongIndex;
|
||||
});
|
||||
if (previousEntry != carousel.end()) {
|
||||
previousCarouselEntry = static_cast<size_t>(
|
||||
std::distance(carousel.begin(), previousEntry));
|
||||
}
|
||||
}
|
||||
const float scrollProgress = motion == MenuMotion::SongScroll
|
||||
? saturate(activeMotionSeconds / kSongScrollSeconds) : 1.0f;
|
||||
float musicLayerDx = 0.0f;
|
||||
float musicLayerAlpha = 1.0f;
|
||||
if (motion == MenuMotion::MusicEnter) {
|
||||
const float progress = saturate(activeMotionSeconds / kMenuEnterSeconds);
|
||||
const float remaining = 1.0f - progress;
|
||||
musicLayerDx = -static_cast<float>(kUiWidth) * remaining * remaining * remaining * remaining;
|
||||
musicLayerAlpha = progress;
|
||||
} else if (motion == MenuMotion::MusicExit) {
|
||||
const float progress = saturate(activeMotionSeconds / kMenuExitSeconds);
|
||||
const float eased = progress * progress * progress * progress;
|
||||
musicLayerDx = static_cast<float>(kUiWidth) * eased;
|
||||
musicLayerAlpha = 1.0f - progress;
|
||||
}
|
||||
const auto animatedSlot = [&](size_t targetSlot) {
|
||||
std::array<float, 4> geometry{
|
||||
kCarouselRowY[targetSlot], kCarouselIndexY[targetSlot],
|
||||
kCarouselCentersY[targetSlot], kCarouselAlpha[targetSlot]
|
||||
};
|
||||
if (motion != MenuMotion::SongScroll || carousel.empty()) return geometry;
|
||||
|
||||
int targetLogical = static_cast<int>(selectedCarouselEntry) +
|
||||
kCarouselOffsets[targetSlot];
|
||||
const int count = static_cast<int>(carousel.size());
|
||||
targetLogical %= count;
|
||||
if (targetLogical < 0) targetLogical += count;
|
||||
const int sourceOffset = wrappedCarouselDistance(
|
||||
previousCarouselEntry, static_cast<size_t>(targetLogical), carousel.size());
|
||||
const int sourceSlot = carouselSlotForOffset(
|
||||
sourceOffset, sourceOffset == 0 && scrollDelta < 0);
|
||||
if (sourceSlot >= 0) {
|
||||
geometry[0] = lerp(kCarouselRowY[static_cast<size_t>(sourceSlot)],
|
||||
geometry[0], scrollProgress);
|
||||
geometry[1] = lerp(kCarouselIndexY[static_cast<size_t>(sourceSlot)],
|
||||
geometry[1], scrollProgress);
|
||||
geometry[2] = lerp(kCarouselCentersY[static_cast<size_t>(sourceSlot)],
|
||||
geometry[2], scrollProgress);
|
||||
geometry[3] = lerp(kCarouselAlpha[static_cast<size_t>(sourceSlot)],
|
||||
geometry[3], scrollProgress);
|
||||
} else {
|
||||
geometry[3] *= scrollProgress;
|
||||
}
|
||||
return geometry;
|
||||
};
|
||||
if (task == SelectTask::Music &&
|
||||
(originalMusicRow.ready() || originalMusicIndex.ready())) {
|
||||
// FUN_00447170 supplies the raw MovieClip translation, while
|
||||
// FUN_00447620 is applied as opacity to these fixed-width rows.
|
||||
// FUN_00446cb0 switches each slot between mc_music_link and
|
||||
// mc_index_link for pseudo IDs >= 50000.
|
||||
static constexpr std::array<int, 12> offsets{
|
||||
-5, -4, -3, -2, -1, 0, 0, 1, 2, 3, 4, 5
|
||||
};
|
||||
static constexpr std::array<float, 12> rowY{
|
||||
175, 228, 281, 334, 387, 440, 701, 754, 807, 860, 913, 966
|
||||
};
|
||||
static constexpr std::array<float, 12> indexY{
|
||||
189, 242, 295, 348, 401, 454, 715, 768, 821, 874, 927, 980
|
||||
};
|
||||
static constexpr std::array<float, 12> rowAlpha{
|
||||
0.0f, 0.7f, 0.8f, 0.9f, 1.0f, 0.0f,
|
||||
0.0f, 1.0f, 0.9f, 0.8f, 0.7f, 0.0f
|
||||
};
|
||||
for (size_t slot = 0; slot < rowY.size(); ++slot) {
|
||||
if (rowAlpha[slot] <= 0.0f || carousel.empty()) continue;
|
||||
const CarouselEntry& entry = carouselAtOffset(offsets[slot]);
|
||||
for (size_t slot = 0; slot < kCarouselRowY.size(); ++slot) {
|
||||
if (carousel.empty()) continue;
|
||||
if (kCarouselOffsets[slot] == 0 &&
|
||||
((scrollDelta >= 0 && slot == 5) || (scrollDelta < 0 && slot == 6))) {
|
||||
continue;
|
||||
}
|
||||
const auto geometry = animatedSlot(slot);
|
||||
const float alpha = geometry[3] * musicLayerAlpha;
|
||||
if (alpha <= 0.0f) continue;
|
||||
const CarouselEntry& entry = carouselAtOffset(kCarouselOffsets[slot]);
|
||||
if (!entry.category && originalMusicRow.ready()) {
|
||||
originalMusicRow.render(ui, 8.0f, rowY[slot], rowAlpha[slot]);
|
||||
originalMusicRow.render(ui, 8.0f + musicLayerDx, geometry[0], alpha);
|
||||
} else if (entry.category && originalMusicIndex.ready()) {
|
||||
originalMusicIndex.render(ui, 88.0f, indexY[slot], rowAlpha[slot]);
|
||||
originalMusicIndex.render(ui, 88.0f + musicLayerDx, geometry[1], alpha);
|
||||
if (genreLabels.id != 0) {
|
||||
const float sourceY = 32.0f * genreLabelRow(entry.genre);
|
||||
// FUN_005aca40: index X/Y plus 53 and 2; FUN_005b3fc0
|
||||
// copies a 256x32 cell from s_j_eng.dds.
|
||||
ui.texture(genreLabels, 141.0f, indexY[slot] + 2.0f,
|
||||
ui.texture(genreLabels, 141.0f + musicLayerDx, geometry[1] + 2.0f,
|
||||
256.0f, 32.0f, 0.0f, sourceY, 256.0f, 32.0f,
|
||||
glm::vec4(1.0f, 1.0f, 1.0f, rowAlpha[slot]));
|
||||
glm::vec4(1.0f, 1.0f, 1.0f, alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1068,6 +1395,18 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
selectedTexture = textures.emplace(selectedSongIndex, loaded).first;
|
||||
}
|
||||
}
|
||||
auto previousTexture = textures.end();
|
||||
if (motion == MenuMotion::SongScroll && previousSelection < visible.size()) {
|
||||
const size_t previousSongIndex = visible[previousSelection];
|
||||
previousTexture = textures.find(previousSongIndex);
|
||||
if (previousTexture == textures.end()) {
|
||||
DdsTexture loaded;
|
||||
if (loadSongMenuTexture(songs[previousSongIndex].menuTexture.string(), loaded,
|
||||
nullptr)) {
|
||||
previousTexture = textures.emplace(previousSongIndex, loaded).first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!usingOriginal) {
|
||||
const glm::vec4 orange{1.0f, 0.31f, 0.08f, 1.0f};
|
||||
@@ -1165,26 +1504,35 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
if (task == SelectTask::Music) {
|
||||
// FUN_005aca40 + FUN_005b34f0: fixed focus fragments from the
|
||||
// selected song's original 512x256 menu atlas.
|
||||
ui.texture(atlas, 39, 497, 196, 196, 1, 1, 196, 196);
|
||||
ui.texture(atlas, 251, 467, 374, 34, 0, 197, 374, 34);
|
||||
ui.texture(atlas, 262, 500, 374, 24, 0, 232, 374, 24);
|
||||
ui.texture(atlas, 262, 522, 314, 16, 198, 180, 314, 16);
|
||||
const auto renderFocus = [&](const DdsTexture& focusAtlas, float alpha) {
|
||||
const glm::vec4 tint{1.0f, 1.0f, 1.0f, alpha * musicLayerAlpha};
|
||||
ui.texture(focusAtlas, 39 + musicLayerDx, 497, 196, 196,
|
||||
1, 1, 196, 196, tint);
|
||||
ui.texture(focusAtlas, 251 + musicLayerDx, 467, 374, 34,
|
||||
0, 197, 374, 34, tint);
|
||||
ui.texture(focusAtlas, 262 + musicLayerDx, 500, 374, 24,
|
||||
0, 232, 374, 24, tint);
|
||||
ui.texture(focusAtlas, 262 + musicLayerDx, 522, 314, 16,
|
||||
198, 180, 314, 16, tint);
|
||||
};
|
||||
if (motion == MenuMotion::SongScroll && previousTexture != textures.end()) {
|
||||
renderFocus(previousTexture->second, 1.0f - scrollProgress);
|
||||
renderFocus(atlas, scrollProgress);
|
||||
} else {
|
||||
renderFocus(atlas, 1.0f);
|
||||
}
|
||||
|
||||
// FUN_00447170/FUN_00447620: the twelve real carousel slots.
|
||||
static constexpr std::array<int, 12> offsets{
|
||||
-5, -4, -3, -2, -1, 0, 0, 1, 2, 3, 4, 5
|
||||
};
|
||||
static constexpr std::array<float, 12> centersY{
|
||||
209, 262, 315, 368, 421, 474, 735, 788, 841, 894, 947, 1000
|
||||
};
|
||||
static constexpr std::array<float, 12> scales{
|
||||
0.0f, 0.7f, 0.8f, 0.9f, 1.0f, 0.0f,
|
||||
0.0f, 1.0f, 0.9f, 0.8f, 0.7f, 0.0f
|
||||
};
|
||||
for (size_t slot = 0; slot < offsets.size(); ++slot) {
|
||||
const float scale = scales[slot];
|
||||
for (size_t slot = 0; slot < kCarouselOffsets.size(); ++slot) {
|
||||
if (kCarouselOffsets[slot] == 0 &&
|
||||
((scrollDelta >= 0 && slot == 5) || (scrollDelta < 0 && slot == 6))) {
|
||||
continue;
|
||||
}
|
||||
const auto geometry = animatedSlot(slot);
|
||||
const float scale = geometry[3];
|
||||
const float alpha = scale * musicLayerAlpha;
|
||||
if (scale <= 0.0f || carousel.empty()) continue;
|
||||
const CarouselEntry& entry = carouselAtOffset(offsets[slot]);
|
||||
const CarouselEntry& entry = carouselAtOffset(kCarouselOffsets[slot]);
|
||||
if (entry.category) continue;
|
||||
const size_t songIndex = entry.songIndex;
|
||||
auto texture = textures.find(songIndex);
|
||||
@@ -1197,19 +1545,28 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
if (texture == textures.end()) continue;
|
||||
const float width = 374.0f * scale;
|
||||
const float height = 34.0f * scale;
|
||||
ui.texture(texture->second, 201.0f - width * 0.5f,
|
||||
centersY[slot] - height * 0.5f, width, height,
|
||||
0, 197, 374, 34);
|
||||
ui.texture(texture->second, 201.0f + musicLayerDx - width * 0.5f,
|
||||
geometry[2] - height * 0.5f, width, height,
|
||||
0, 197, 374, 34,
|
||||
glm::vec4(1.0f, 1.0f, 1.0f, alpha));
|
||||
}
|
||||
} else {
|
||||
// Fixed selected-song fragments in CDifficultyTask's renderer
|
||||
// (FUN_005be2d0), using its executable constants.
|
||||
// FUN_005b33a0 treats the executable constants as sprite
|
||||
// centres and subtracts half of the scaled source size.
|
||||
ui.texture(atlas, 105, 167, 98, 98, 1, 1, 196, 196);
|
||||
ui.texture(atlas, 209, 178, 374, 34, 0, 197, 374, 34);
|
||||
ui.texture(atlas, 220, 214, 374, 24, 0, 232, 374, 24);
|
||||
ui.texture(atlas, 220, 239, 314, 16, 198, 180, 314, 16);
|
||||
float modeAlpha = 1.0f;
|
||||
if (motion == MenuMotion::ModeEnter) {
|
||||
modeAlpha = saturate(activeMotionSeconds / kMenuEnterSeconds);
|
||||
} else if (motion == MenuMotion::ModeExitBack ||
|
||||
motion == MenuMotion::ModeExitConfirm) {
|
||||
modeAlpha = 1.0f - saturate(activeMotionSeconds / kMenuExitSeconds);
|
||||
}
|
||||
const glm::vec4 tint{1.0f, 1.0f, 1.0f, modeAlpha};
|
||||
ui.texture(atlas, 105, 167, 98, 98, 1, 1, 196, 196, tint);
|
||||
ui.texture(atlas, 209, 178, 374, 34, 0, 197, 374, 34, tint);
|
||||
ui.texture(atlas, 220, 214, 374, 24, 0, 232, 374, 24, tint);
|
||||
ui.texture(atlas, 220, 239, 314, 16, 198, 180, 314, 16, tint);
|
||||
}
|
||||
}
|
||||
// The navigator and common HUD are later compositing passes in
|
||||
@@ -1218,11 +1575,28 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
||||
if (originalCommon.ready()) originalCommon.render(ui);
|
||||
ui.flush();
|
||||
|
||||
if (capturePrefix && task == SelectTask::Music && !capturedMusic) {
|
||||
if (capturePrefix && captureTransitions && !capturedMusicDecision &&
|
||||
motion == MenuMotion::MusicDecision &&
|
||||
activeMotionSeconds >= kDecisionSeconds * 0.5f) {
|
||||
captureFrameForReverse(capturePrefix, "music-decision", pixelWidth, pixelHeight);
|
||||
capturedMusicDecision = true;
|
||||
} else if (capturePrefix && captureTransitions && !capturedMusicExit &&
|
||||
motion == MenuMotion::MusicExit &&
|
||||
activeMotionSeconds >= kMenuExitSeconds * 0.5f) {
|
||||
captureFrameForReverse(capturePrefix, "music-exit", pixelWidth, pixelHeight);
|
||||
capturedMusicExit = true;
|
||||
} else if (capturePrefix && captureTransitions && !capturedModeEnter &&
|
||||
motion == MenuMotion::ModeEnter &&
|
||||
activeMotionSeconds >= kMenuEnterSeconds * 0.5f) {
|
||||
captureFrameForReverse(capturePrefix, "mode-enter", pixelWidth, pixelHeight);
|
||||
capturedModeEnter = true;
|
||||
} else if (capturePrefix && motion == MenuMotion::Idle &&
|
||||
task == SelectTask::Music && !capturedMusic) {
|
||||
captureFrameForReverse(capturePrefix, "music", pixelWidth, pixelHeight);
|
||||
capturedMusic = true;
|
||||
if (captureBoth) task = SelectTask::Difficulty;
|
||||
} else if (capturePrefix && task == SelectTask::Difficulty && !capturedDifficulty) {
|
||||
if (captureBoth) startMotion(MenuMotion::MusicDecision);
|
||||
} else if (capturePrefix && motion == MenuMotion::Idle &&
|
||||
task == SelectTask::Difficulty && !capturedDifficulty) {
|
||||
captureFrameForReverse(capturePrefix, "difficulty", pixelWidth, pixelHeight);
|
||||
capturedDifficulty = true;
|
||||
}
|
||||
|
||||
+14
-6
@@ -90,9 +90,11 @@ list deltas rather than complete scenes:
|
||||
- `COLT` carries RGBA multiplication, including authored visibility fades;
|
||||
- `ASRC` contains the exported `play();`, `stop();`, and target actions.
|
||||
|
||||
`gc::BuildRvbSnapshot` now accumulates those deltas through a selected label
|
||||
and advances `play()` entry frames to their following `stop()` frame. This is
|
||||
why hidden templates and transition masks no longer appear together.
|
||||
`gc::BuildRvbSnapshot` accumulates those deltas through a selected label. Its
|
||||
snapshot state can either advance a `play()` entry to the following `stop()`
|
||||
or request a relative animation frame globally or per MovieClip path. Per-path
|
||||
evaluation is required here because the selected transition must advance while
|
||||
unrelated child loops remain at their authored steady frames.
|
||||
|
||||
MTX starts with `MTX\0`; each payload is a DDS whose first dword was replaced
|
||||
by the container. Restoring `DDS ` yields a standard DDS. RVB `ImageN` maps to
|
||||
@@ -137,9 +139,15 @@ actual formulas and spacings:
|
||||
- `(7 - count) * 0.5 * 68`, with the row anchored at `426 + 16`;
|
||||
- `(9 - count) * 0.5 * 52`, with the row anchored at `433 + 16`.
|
||||
|
||||
The remaining visual work is outside this recovered static scene snapshot:
|
||||
the executable-owned player/status HUD in the blank upper band, continuous
|
||||
timeline interpolation, and the exact transition timing between task states.
|
||||
`FUN_00447170` also supplies the transition timing used by the executable. Song
|
||||
slot changes interpolate linearly over `0.125` seconds. The list enters/exits
|
||||
over `0.375` seconds and shifts by one viewport width using exponent `4` easing.
|
||||
The row plates retain their full dimensions; their opacity and the title scale
|
||||
use the slot values `0.7`, `0.8`, `0.9`, and `1.0`. The desktop selector now
|
||||
combines these executable-owned values with frame-based RVB entry, exit, sort,
|
||||
focus, difficulty-change, and decision states. Confirmation is a distinct
|
||||
`jf_decision`/`jf_mode_decision` phase before the task exit timeline; collapsing
|
||||
both phases is why an immediate screen switch does not resemble the cabinet.
|
||||
|
||||
## Common and navigator layers
|
||||
|
||||
|
||||
Reference in New Issue
Block a user