forked from tsuki/openroller
Reproduce arcade menu transitions
This commit is contained in:
+434
-60
@@ -17,6 +17,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <initializer_list>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -45,6 +46,67 @@ enum class SelectTask {
|
|||||||
Difficulty,
|
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 {
|
struct Song {
|
||||||
gc::StageCatalogEntry catalog;
|
gc::StageCatalogEntry catalog;
|
||||||
fs::path menuTexture;
|
fs::path menuTexture;
|
||||||
@@ -433,9 +495,16 @@ public:
|
|||||||
const auto found = textures_.find(draw.imageSymbol);
|
const auto found = textures_.find(draw.imageSymbol);
|
||||||
if (found == textures_.end()) continue;
|
if (found == textures_.end()) continue;
|
||||||
auto corners = draw.corners;
|
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) {
|
for (auto& corner : corners) {
|
||||||
corner[0] += dx;
|
corner[0] += dx + symbolDx;
|
||||||
corner[1] += dy;
|
corner[1] += dy + symbolDy;
|
||||||
}
|
}
|
||||||
ui.textureQuad(found->second, corners,
|
ui.textureQuad(found->second, corners,
|
||||||
glm::vec4(draw.color[0], draw.color[1], draw.color[2],
|
glm::vec4(draw.color[0], draw.color[1], draw.color[2],
|
||||||
@@ -446,13 +515,7 @@ public:
|
|||||||
bool ready() const { return !textures_.empty(); }
|
bool ready() const { return !textures_.empty(); }
|
||||||
|
|
||||||
void translateSymbol(const std::string& symbol, float dx, float dy) {
|
void translateSymbol(const std::string& symbol, float dx, float dy) {
|
||||||
for (gc::RvbImageDraw& draw : draws_) {
|
symbolTranslations_[symbol] = {dx, dy};
|
||||||
if (draw.imageSymbol != symbol) continue;
|
|
||||||
for (auto& corner : draw.corners) {
|
|
||||||
corner[0] += dx;
|
|
||||||
corner[1] += dy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
@@ -460,6 +523,7 @@ public:
|
|||||||
if (texture.id != 0) glDeleteTextures(1, &texture.id);
|
if (texture.id != 0) glDeleteTextures(1, &texture.id);
|
||||||
}
|
}
|
||||||
textures_.clear();
|
textures_.clear();
|
||||||
|
symbolTranslations_.clear();
|
||||||
draws_.clear();
|
draws_.clear();
|
||||||
rvbBytes_.clear();
|
rvbBytes_.clear();
|
||||||
mtxBytes_.clear();
|
mtxBytes_.clear();
|
||||||
@@ -488,6 +552,7 @@ private:
|
|||||||
}
|
}
|
||||||
std::vector<gc::RvbImageDraw> draws_;
|
std::vector<gc::RvbImageDraw> draws_;
|
||||||
std::unordered_map<std::string, DdsTexture> textures_;
|
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> rvbBytes_;
|
||||||
std::vector<uint8_t> mtxBytes_;
|
std::vector<uint8_t> mtxBytes_;
|
||||||
gc::RvbScene scene_;
|
gc::RvbScene scene_;
|
||||||
@@ -563,6 +628,22 @@ gc::RvbSnapshotState selectMusicSceneState(const Song* song = nullptr,
|
|||||||
return state;
|
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 selectModeSceneState(const Song* song = nullptr, int difficulty = 0) {
|
||||||
gc::RvbSnapshotState state;
|
gc::RvbSnapshotState state;
|
||||||
auto& frame = state.frameByPath;
|
auto& frame = state.frameByPath;
|
||||||
@@ -604,6 +685,16 @@ gc::RvbSnapshotState selectModeSceneState(const Song* song = nullptr, int diffic
|
|||||||
return state;
|
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 commonSelectSceneState() {
|
||||||
gc::RvbSnapshotState state;
|
gc::RvbSnapshotState state;
|
||||||
auto& frame = state.frameByPath;
|
auto& frame = state.frameByPath;
|
||||||
@@ -623,6 +714,15 @@ gc::RvbSnapshotState commonSelectSceneState() {
|
|||||||
return state;
|
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 navigatorSelectSceneState() {
|
||||||
gc::RvbSnapshotState state;
|
gc::RvbSnapshotState state;
|
||||||
auto& frame = state.frameByPath;
|
auto& frame = state.frameByPath;
|
||||||
@@ -633,6 +733,21 @@ gc::RvbSnapshotState navigatorSelectSceneState() {
|
|||||||
return state;
|
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) {
|
int genreLabelRow(int genre) {
|
||||||
// s_j[_eng].dds is the executable-owned 256x256 genre-label atlas.
|
// s_j[_eng].dds is the executable-owned 256x256 genre-label atlas.
|
||||||
// Each pseudo song ID 50000+n selects one 256x32 row.
|
// 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();
|
rebuildVisible();
|
||||||
|
|
||||||
size_t selection = 0;
|
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]]);
|
int difficulty = firstPlayableDifficulty(songs[visible[selection]]);
|
||||||
auto refreshOriginalScenes = [&] {
|
auto refreshOriginalScenes = [&] {
|
||||||
if (visible.empty()) return;
|
if (visible.empty()) return;
|
||||||
@@ -888,12 +1011,15 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
refreshOriginalScenes();
|
refreshOriginalScenes();
|
||||||
auto changeSong = [&](int delta) {
|
auto changeSong = [&](int delta) {
|
||||||
if (visible.empty()) return;
|
if (visible.empty()) return;
|
||||||
|
previousSelection = selection;
|
||||||
|
scrollDelta = delta;
|
||||||
const int count = static_cast<int>(visible.size());
|
const int count = static_cast<int>(visible.size());
|
||||||
int next = (static_cast<int>(selection) + delta) % count;
|
int next = (static_cast<int>(selection) + delta) % count;
|
||||||
if (next < 0) next += count;
|
if (next < 0) next += count;
|
||||||
selection = static_cast<size_t>(next);
|
selection = static_cast<size_t>(next);
|
||||||
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
|
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
|
||||||
refreshOriginalScenes();
|
refreshOriginalScenes();
|
||||||
|
startMotion(MenuMotion::SongScroll);
|
||||||
};
|
};
|
||||||
auto changeSort = [&](int delta) {
|
auto changeSort = [&](int delta) {
|
||||||
const size_t selectedSong = visible.empty() ? 0 : visible[selection];
|
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);
|
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
|
||||||
}
|
}
|
||||||
refreshOriginalScenes();
|
refreshOriginalScenes();
|
||||||
|
startMotion(MenuMotion::SortChange);
|
||||||
};
|
};
|
||||||
auto changeDifficulty = [&](int delta) {
|
auto changeDifficulty = [&](int delta) {
|
||||||
const Song& song = songs[visible[selection]];
|
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()) {
|
if (!song.stages[next].empty()) {
|
||||||
difficulty = next;
|
difficulty = next;
|
||||||
refreshOriginalScenes();
|
refreshOriginalScenes();
|
||||||
|
startMotion(MenuMotion::DifficultyChange);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -927,10 +1055,78 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
SelectTask task = SelectTask::Music;
|
SelectTask task = SelectTask::Music;
|
||||||
bool capturedMusic = false;
|
bool capturedMusic = false;
|
||||||
bool capturedDifficulty = false;
|
bool capturedDifficulty = false;
|
||||||
|
bool capturedMusicDecision = false;
|
||||||
|
bool capturedMusicExit = false;
|
||||||
|
bool capturedModeEnter = false;
|
||||||
const char* capturePrefix = std::getenv("OPENROLLER_CAPTURE_PREFIX");
|
const char* capturePrefix = std::getenv("OPENROLLER_CAPTURE_PREFIX");
|
||||||
const bool captureBoth = std::getenv("OPENROLLER_CAPTURE_BOTH") != nullptr;
|
const bool captureBoth = std::getenv("OPENROLLER_CAPTURE_BOTH") != nullptr;
|
||||||
|
const bool captureTransitions =
|
||||||
|
std::getenv("OPENROLLER_CAPTURE_TRANSITIONS") != nullptr;
|
||||||
const Uint64 menuStartTicks = SDL_GetTicks();
|
const Uint64 menuStartTicks = SDL_GetTicks();
|
||||||
while (running) {
|
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;
|
SDL_Event event;
|
||||||
while (SDL_PollEvent(&event)) {
|
while (SDL_PollEvent(&event)) {
|
||||||
if (event.type == SDL_EVENT_QUIT) running = false;
|
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");
|
SDL_SetWindowTitle(window, "OpenRoller");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (motion != MenuMotion::Idle) continue;
|
||||||
if (task == SelectTask::Music) {
|
if (task == SelectTask::Music) {
|
||||||
switch (event.key.key) {
|
switch (event.key.key) {
|
||||||
case SDLK_ESCAPE: running = false; break;
|
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_Q: changeSort(-1); break;
|
||||||
case SDLK_E: case SDLK_TAB: changeSort(1); break;
|
case SDLK_E: case SDLK_TAB: changeSort(1); break;
|
||||||
case SDLK_RETURN: case SDLK_SPACE:
|
case SDLK_RETURN: case SDLK_SPACE:
|
||||||
task = SelectTask::Difficulty;
|
startMotion(MenuMotion::MusicDecision);
|
||||||
break;
|
break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (event.key.key) {
|
switch (event.key.key) {
|
||||||
case SDLK_ESCAPE:
|
case SDLK_ESCAPE:
|
||||||
task = SelectTask::Music;
|
startMotion(MenuMotion::ModeExitBack);
|
||||||
break;
|
break;
|
||||||
case SDLK_UP: case SDLK_W: case SDLK_LEFT: case SDLK_A:
|
case SDLK_UP: case SDLK_W: case SDLK_LEFT: case SDLK_A:
|
||||||
changeDifficulty(-1);
|
changeDifficulty(-1);
|
||||||
@@ -966,8 +1163,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
changeDifficulty(1);
|
changeDifficulty(1);
|
||||||
break;
|
break;
|
||||||
case SDLK_RETURN: case SDLK_SPACE:
|
case SDLK_RETURN: case SDLK_SPACE:
|
||||||
confirmed = true;
|
startMotion(MenuMotion::ModeDecision);
|
||||||
running = false;
|
|
||||||
break;
|
break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
@@ -976,6 +1172,86 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
if (visible.empty()) continue;
|
if (visible.empty()) continue;
|
||||||
|
|
||||||
const Song& selected = songs[visible[selection]];
|
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());
|
SDL_SetWindowTitle(window, ("OpenRoller - " + selected.catalog.imageKey).c_str());
|
||||||
int pixelWidth = 0, pixelHeight = 0;
|
int pixelWidth = 0, pixelHeight = 0;
|
||||||
SDL_GetWindowSizeInPixels(window, &pixelWidth, &pixelHeight);
|
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;
|
if (logical < 0) logical += count;
|
||||||
return carousel[static_cast<size_t>(logical)];
|
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 &&
|
if (task == SelectTask::Music &&
|
||||||
(originalMusicRow.ready() || originalMusicIndex.ready())) {
|
(originalMusicRow.ready() || originalMusicIndex.ready())) {
|
||||||
// FUN_00447170 supplies the raw MovieClip translation, while
|
// FUN_00447170 supplies the raw MovieClip translation, while
|
||||||
// FUN_00447620 is applied as opacity to these fixed-width rows.
|
// FUN_00447620 is applied as opacity to these fixed-width rows.
|
||||||
// FUN_00446cb0 switches each slot between mc_music_link and
|
// FUN_00446cb0 switches each slot between mc_music_link and
|
||||||
// mc_index_link for pseudo IDs >= 50000.
|
// mc_index_link for pseudo IDs >= 50000.
|
||||||
static constexpr std::array<int, 12> offsets{
|
for (size_t slot = 0; slot < kCarouselRowY.size(); ++slot) {
|
||||||
-5, -4, -3, -2, -1, 0, 0, 1, 2, 3, 4, 5
|
if (carousel.empty()) continue;
|
||||||
};
|
if (kCarouselOffsets[slot] == 0 &&
|
||||||
static constexpr std::array<float, 12> rowY{
|
((scrollDelta >= 0 && slot == 5) || (scrollDelta < 0 && slot == 6))) {
|
||||||
175, 228, 281, 334, 387, 440, 701, 754, 807, 860, 913, 966
|
continue;
|
||||||
};
|
}
|
||||||
static constexpr std::array<float, 12> indexY{
|
const auto geometry = animatedSlot(slot);
|
||||||
189, 242, 295, 348, 401, 454, 715, 768, 821, 874, 927, 980
|
const float alpha = geometry[3] * musicLayerAlpha;
|
||||||
};
|
if (alpha <= 0.0f) continue;
|
||||||
static constexpr std::array<float, 12> rowAlpha{
|
const CarouselEntry& entry = carouselAtOffset(kCarouselOffsets[slot]);
|
||||||
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]);
|
|
||||||
if (!entry.category && originalMusicRow.ready()) {
|
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()) {
|
} 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) {
|
if (genreLabels.id != 0) {
|
||||||
const float sourceY = 32.0f * genreLabelRow(entry.genre);
|
const float sourceY = 32.0f * genreLabelRow(entry.genre);
|
||||||
// FUN_005aca40: index X/Y plus 53 and 2; FUN_005b3fc0
|
// FUN_005aca40: index X/Y plus 53 and 2; FUN_005b3fc0
|
||||||
// copies a 256x32 cell from s_j_eng.dds.
|
// 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,
|
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;
|
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) {
|
if (!usingOriginal) {
|
||||||
const glm::vec4 orange{1.0f, 0.31f, 0.08f, 1.0f};
|
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) {
|
if (task == SelectTask::Music) {
|
||||||
// FUN_005aca40 + FUN_005b34f0: fixed focus fragments from the
|
// FUN_005aca40 + FUN_005b34f0: fixed focus fragments from the
|
||||||
// selected song's original 512x256 menu atlas.
|
// selected song's original 512x256 menu atlas.
|
||||||
ui.texture(atlas, 39, 497, 196, 196, 1, 1, 196, 196);
|
const auto renderFocus = [&](const DdsTexture& focusAtlas, float alpha) {
|
||||||
ui.texture(atlas, 251, 467, 374, 34, 0, 197, 374, 34);
|
const glm::vec4 tint{1.0f, 1.0f, 1.0f, alpha * musicLayerAlpha};
|
||||||
ui.texture(atlas, 262, 500, 374, 24, 0, 232, 374, 24);
|
ui.texture(focusAtlas, 39 + musicLayerDx, 497, 196, 196,
|
||||||
ui.texture(atlas, 262, 522, 314, 16, 198, 180, 314, 16);
|
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.
|
// FUN_00447170/FUN_00447620: the twelve real carousel slots.
|
||||||
static constexpr std::array<int, 12> offsets{
|
for (size_t slot = 0; slot < kCarouselOffsets.size(); ++slot) {
|
||||||
-5, -4, -3, -2, -1, 0, 0, 1, 2, 3, 4, 5
|
if (kCarouselOffsets[slot] == 0 &&
|
||||||
};
|
((scrollDelta >= 0 && slot == 5) || (scrollDelta < 0 && slot == 6))) {
|
||||||
static constexpr std::array<float, 12> centersY{
|
continue;
|
||||||
209, 262, 315, 368, 421, 474, 735, 788, 841, 894, 947, 1000
|
}
|
||||||
};
|
const auto geometry = animatedSlot(slot);
|
||||||
static constexpr std::array<float, 12> scales{
|
const float scale = geometry[3];
|
||||||
0.0f, 0.7f, 0.8f, 0.9f, 1.0f, 0.0f,
|
const float alpha = scale * musicLayerAlpha;
|
||||||
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];
|
|
||||||
if (scale <= 0.0f || carousel.empty()) continue;
|
if (scale <= 0.0f || carousel.empty()) continue;
|
||||||
const CarouselEntry& entry = carouselAtOffset(offsets[slot]);
|
const CarouselEntry& entry = carouselAtOffset(kCarouselOffsets[slot]);
|
||||||
if (entry.category) continue;
|
if (entry.category) continue;
|
||||||
const size_t songIndex = entry.songIndex;
|
const size_t songIndex = entry.songIndex;
|
||||||
auto texture = textures.find(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;
|
if (texture == textures.end()) continue;
|
||||||
const float width = 374.0f * scale;
|
const float width = 374.0f * scale;
|
||||||
const float height = 34.0f * scale;
|
const float height = 34.0f * scale;
|
||||||
ui.texture(texture->second, 201.0f - width * 0.5f,
|
ui.texture(texture->second, 201.0f + musicLayerDx - width * 0.5f,
|
||||||
centersY[slot] - height * 0.5f, width, height,
|
geometry[2] - height * 0.5f, width, height,
|
||||||
0, 197, 374, 34);
|
0, 197, 374, 34,
|
||||||
|
glm::vec4(1.0f, 1.0f, 1.0f, alpha));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fixed selected-song fragments in CDifficultyTask's renderer
|
// Fixed selected-song fragments in CDifficultyTask's renderer
|
||||||
// (FUN_005be2d0), using its executable constants.
|
// (FUN_005be2d0), using its executable constants.
|
||||||
// FUN_005b33a0 treats the executable constants as sprite
|
// FUN_005b33a0 treats the executable constants as sprite
|
||||||
// centres and subtracts half of the scaled source size.
|
// centres and subtracts half of the scaled source size.
|
||||||
ui.texture(atlas, 105, 167, 98, 98, 1, 1, 196, 196);
|
float modeAlpha = 1.0f;
|
||||||
ui.texture(atlas, 209, 178, 374, 34, 0, 197, 374, 34);
|
if (motion == MenuMotion::ModeEnter) {
|
||||||
ui.texture(atlas, 220, 214, 374, 24, 0, 232, 374, 24);
|
modeAlpha = saturate(activeMotionSeconds / kMenuEnterSeconds);
|
||||||
ui.texture(atlas, 220, 239, 314, 16, 198, 180, 314, 16);
|
} 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
|
// 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);
|
if (originalCommon.ready()) originalCommon.render(ui);
|
||||||
ui.flush();
|
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);
|
captureFrameForReverse(capturePrefix, "music", pixelWidth, pixelHeight);
|
||||||
capturedMusic = true;
|
capturedMusic = true;
|
||||||
if (captureBoth) task = SelectTask::Difficulty;
|
if (captureBoth) startMotion(MenuMotion::MusicDecision);
|
||||||
} else if (capturePrefix && task == SelectTask::Difficulty && !capturedDifficulty) {
|
} else if (capturePrefix && motion == MenuMotion::Idle &&
|
||||||
|
task == SelectTask::Difficulty && !capturedDifficulty) {
|
||||||
captureFrameForReverse(capturePrefix, "difficulty", pixelWidth, pixelHeight);
|
captureFrameForReverse(capturePrefix, "difficulty", pixelWidth, pixelHeight);
|
||||||
capturedDifficulty = true;
|
capturedDifficulty = true;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -90,9 +90,11 @@ list deltas rather than complete scenes:
|
|||||||
- `COLT` carries RGBA multiplication, including authored visibility fades;
|
- `COLT` carries RGBA multiplication, including authored visibility fades;
|
||||||
- `ASRC` contains the exported `play();`, `stop();`, and target actions.
|
- `ASRC` contains the exported `play();`, `stop();`, and target actions.
|
||||||
|
|
||||||
`gc::BuildRvbSnapshot` now accumulates those deltas through a selected label
|
`gc::BuildRvbSnapshot` accumulates those deltas through a selected label. Its
|
||||||
and advances `play()` entry frames to their following `stop()` frame. This is
|
snapshot state can either advance a `play()` entry to the following `stop()`
|
||||||
why hidden templates and transition masks no longer appear together.
|
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
|
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
|
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`;
|
- `(7 - count) * 0.5 * 68`, with the row anchored at `426 + 16`;
|
||||||
- `(9 - count) * 0.5 * 52`, with the row anchored at `433 + 16`.
|
- `(9 - count) * 0.5 * 52`, with the row anchored at `433 + 16`.
|
||||||
|
|
||||||
The remaining visual work is outside this recovered static scene snapshot:
|
`FUN_00447170` also supplies the transition timing used by the executable. Song
|
||||||
the executable-owned player/status HUD in the blank upper band, continuous
|
slot changes interpolate linearly over `0.125` seconds. The list enters/exits
|
||||||
timeline interpolation, and the exact transition timing between task states.
|
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
|
## Common and navigator layers
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user