From 0be0ff963e38fb2e50c094b5ed3d558c5c32bb24 Mon Sep 17 00:00:00 2001 From: Gordon Weeks <627684+gcweeks@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:19:40 -0800 Subject: [PATCH] Added song selection. --- assets/songs/json/mary.json | 3 +- src/entities/song.h | 4 +- src/main.cpp | 3 + src/samples/ghhb_game.h | 20 ++-- src/samples/instrument_select.h | 7 +- src/samples/song_select.h | 187 ++++++++++++++++++++++++++++++++ src/samples/title_screen.h | 2 +- 7 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 src/samples/song_select.h diff --git a/assets/songs/json/mary.json b/assets/songs/json/mary.json index a438306..bf07e33 100644 --- a/assets/songs/json/mary.json +++ b/assets/songs/json/mary.json @@ -2,7 +2,8 @@ "header": { "keySignatures": [], "meta": [], - "name": "", + "name": "Mary Had A Little Lamb", + "artist": "Mrs Mary", "ppq": 480, "tempos": [ { diff --git a/src/entities/song.h b/src/entities/song.h index 77be8c1..ac4826a 100644 --- a/src/entities/song.h +++ b/src/entities/song.h @@ -28,6 +28,7 @@ struct Track { struct Header { std::string name; + std::string artist; int ppq; float bpm; }; @@ -45,7 +46,8 @@ Song parseSong(const rapidjson::Document& doc) { if (doc.HasMember("header") && doc["header"].IsObject()) { const auto& h = doc["header"].GetObject(); Header header; - header.name = h["name"].GetString(); + header.name = h.HasMember("name") && h["name"].IsString() ? h["name"].GetString() : ""; + header.artist = h.HasMember("artist") && h["artist"].IsString() ? h["artist"].GetString() : ""; header.ppq = h["ppq"].GetInt(); printf("header has tempos: %d\n", h.HasMember("tempos")); if (h.HasMember("tempos") && h["tempos"].IsArray()) { diff --git a/src/main.cpp b/src/main.cpp index f51388a..28600ff 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include "samples/ghhb_game.h" #include "samples/instrument_select.h" +#include "samples/song_select.h" #include "samples/title_screen.h" #include "entities/song.h" @@ -10,6 +11,7 @@ int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1}; int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1}; +std::string SELECTED_SONG_PATH = "assets/songs/json/mary.json"; Game game; @@ -34,6 +36,7 @@ int main(int argc, char** argv) font_manager->set_texture_filter("Roboto", TEXTURE_FILTER_BILINEAR); game.add_scene("title"); + game.add_scene("song_select"); game.add_scene("instrument_select"); game.add_scene("ghhb"); diff --git a/src/samples/ghhb_game.h b/src/samples/ghhb_game.h index 704f594..edb45e7 100644 --- a/src/samples/ghhb_game.h +++ b/src/samples/ghhb_game.h @@ -6,6 +6,7 @@ #include "background.h" #include #include +#include #include #include @@ -154,10 +155,9 @@ std::vector load_chart(const char* path) return empty; } -// const char* const GHHB_CHART_PATH = "assets/songs/json/tetris.json"; -const char* const GHHB_CHART_PATH = "assets/songs/json/mary.json"; } // namespace +extern std::string SELECTED_SONG_PATH; extern int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES]; extern int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES]; @@ -192,6 +192,14 @@ public: void on_enter() override { + chart = load_chart(SELECTED_SONG_PATH.c_str()); + float first_note_time = 0.0f; + if (!chart.empty()) + { + first_note_time = chart.front().time; + float lead_seconds = (hit_line_y - 60.0f) / SCROLL_PX_PER_SEC; + chart_time_offset = lead_seconds + LEAD_OFFSET_SECONDS - first_note_time; + } song_time = 0.0f; score = 0; combo = 0; @@ -263,14 +271,6 @@ public: } } } - chart = load_chart(GHHB_CHART_PATH); - float first_note_time = 0.0f; - if (!chart.empty()) - { - first_note_time = chart.front().time; - float lead_seconds = (hit_line_y - 60.0f) / SCROLL_PX_PER_SEC; - chart_time_offset = lead_seconds + LEAD_OFFSET_SECONDS - first_note_time; - } background = add_game_object(); background->add_tag("background"); } diff --git a/src/samples/instrument_select.h b/src/samples/instrument_select.h index c7f2f74..5645521 100644 --- a/src/samples/instrument_select.h +++ b/src/samples/instrument_select.h @@ -79,7 +79,12 @@ public: int slot = BUTTON_SLOT[b]; int gi = get_or_assign_gamepad_index(gp); if (gi >= 0) - instrument_owner[slot] = gi; + { + if (instrument_owner[slot] == gi) + instrument_owner[slot] = -1; + else + instrument_owner[slot] = gi; + } } } } diff --git a/src/samples/song_select.h b/src/samples/song_select.h new file mode 100644 index 0000000..cc9d8b0 --- /dev/null +++ b/src/samples/song_select.h @@ -0,0 +1,187 @@ +#pragma once + +#include "engine/prefabs/includes.h" +#include "entities/song.h" +#include "rapidjson/filereadstream.h" +#include +#include + +extern std::string SELECTED_SONG_PATH; + +struct SongEntry +{ + std::string name; + std::string artist; + std::string path; +}; + +inline std::vector build_song_list(const std::vector& paths) +{ + std::vector entries; + for (const std::string& path : paths) + { + std::FILE* fp = std::fopen(path.c_str(), "rb"); + if (!fp) + continue; + char read_buf[65536]; + rapidjson::FileReadStream is(fp, read_buf, sizeof(read_buf)); + rapidjson::Document doc; + if (doc.ParseStream(is).HasParseError()) + { + std::fclose(fp); + continue; + } + std::fclose(fp); + Song song = parseSong(doc); + entries.push_back( + {song.header.name.empty() ? path : song.header.name, song.header.artist, path}); + } + while (entries.size() < 4) + { + for (size_t i = 0; i < entries.size() && entries.size() < 4; i++) + entries.push_back(entries[i]); + } + return entries; +} + +class SongSelectScreen : public Scene +{ +public: + static constexpr int VISIBLE_SLOTS = 4; + + Font font = {0}; + std::vector songs; + int selected_index = 0; + int scroll_offset = 0; + + void init() override + { + auto font_manager = game->get_manager(); + font = font_manager->get_font("Roboto"); + std::vector paths = {"assets/songs/json/mary.json"}; + songs = build_song_list(paths); + selected_index = 0; + scroll_offset = 0; + } + + void update(float delta_time) override + { + bool up = IsKeyPressed(KEY_UP); + bool down = IsKeyPressed(KEY_DOWN); + for (int gp = 0; gp < 4; gp++) + { + if (IsGamepadAvailable(gp)) + { + if (IsGamepadButtonPressed(gp, GAMEPAD_BUTTON_LEFT_FACE_UP)) + up = true; + if (IsGamepadButtonPressed(gp, GAMEPAD_BUTTON_LEFT_FACE_DOWN)) + down = true; + } + } + int n = static_cast(songs.size()); + if (up) + { + if (selected_index > 0) + selected_index--; + else if (scroll_offset > 0) + scroll_offset--; + else + { + scroll_offset = n > VISIBLE_SLOTS ? n - VISIBLE_SLOTS : 0; + selected_index = n > VISIBLE_SLOTS ? VISIBLE_SLOTS - 1 : n - 1; + } + } + if (down) + { + if (selected_index < VISIBLE_SLOTS - 1 && scroll_offset + selected_index < n - 1) + selected_index++; + else if (scroll_offset + VISIBLE_SLOTS < n) + scroll_offset++; + else + { + scroll_offset = 0; + selected_index = 0; + } + } + + bool confirm = IsKeyPressed(KEY_ENTER); + for (int gp = 0; gp < 4; gp++) + { + if (IsGamepadAvailable(gp) && IsGamepadButtonPressed(gp, GAMEPAD_BUTTON_MIDDLE_RIGHT)) + confirm = true; + } + if (confirm && !songs.empty()) + { + int idx = scroll_offset + selected_index; + if (idx >= 0 && idx < static_cast(songs.size())) + { + SELECTED_SONG_PATH = songs[idx].path; + game->go_to_scene("instrument_select"); + } + } + } + + void draw() override + { + float w = static_cast(GetScreenWidth()); + float h = static_cast(GetScreenHeight()); + + ClearBackground(SKYBLUE); + + const char* title_text = "Select Song"; + float title_size = 48.0f; + float title_w = MeasureTextEx(font, title_text, title_size, 1.0f).x; + DrawTextEx(font, title_text, Vector2{(w - title_w) * 0.5f, 60.0f}, title_size, 1.0f, WHITE); + + float row_height = 80.0f; + float start_y = 180.0f; + float list_center_x = w * 0.5f; + float name_size = 28.0f; + float artist_size = 18.0f; + float artist_offset_x = 0.0f; + Color highlight_bg = {60, 80, 120, 220}; + Color highlight_border = {100, 130, 180, 255}; + float content_height = name_size + 4.0f + artist_size; + float box_pad_v = 6.0f; + float highlight_height = content_height + box_pad_v * 2.0f; + + for (int i = 0; i < VISIBLE_SLOTS; i++) + { + int list_idx = scroll_offset + i; + float y = start_y + i * row_height; + bool selected = (i == selected_index); + + if (list_idx < static_cast(songs.size())) + { + const SongEntry& e = songs[list_idx]; + if (selected) + { + float box_w = w * 0.45f; + float box_x = (w - box_w) * 0.5f; + float box_y = y - box_pad_v; + DrawRectangleRounded( + Rectangle{box_x, box_y, box_w, highlight_height}, 0.2f, 12, highlight_bg); + DrawRectangleRoundedLines( + Rectangle{box_x, box_y, box_w, highlight_height}, 0.2f, 12, + highlight_border); + } + float name_w = MeasureTextEx(font, e.name.c_str(), name_size, 1.0f).x; + DrawTextEx(font, e.name.c_str(), + Vector2{list_center_x - name_w * 0.5f, y}, name_size, 1.0f, WHITE); + if (!e.artist.empty()) + { + float artist_w = MeasureTextEx(font, e.artist.c_str(), artist_size, 1.0f).x; + DrawTextEx(font, e.artist.c_str(), + Vector2{list_center_x - artist_w * 0.5f + artist_offset_x, + y + name_size + 4.0f}, + artist_size, 1.0f, Color{200, 200, 200, 255}); + } + } + } + + const char* hint = "D-Pad Up/Down | Start to confirm"; + float hint_size = 20.0f; + float hint_w = MeasureTextEx(font, hint, hint_size, 1.0f).x; + DrawTextEx(font, hint, Vector2{(w - hint_w) * 0.5f, h - 50.0f}, hint_size, 1.0f, WHITE); + } +}; diff --git a/src/samples/title_screen.h b/src/samples/title_screen.h index 89eebfe..e69dd8e 100644 --- a/src/samples/title_screen.h +++ b/src/samples/title_screen.h @@ -18,7 +18,7 @@ public: // Trigger scene change on Enter key or gamepad start button. if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT)) { - game->go_to_scene("instrument_select"); + game->go_to_scene("song_select"); } }