Added song selection.

This commit is contained in:
Gordon Weeks 2026-01-31 11:19:40 -08:00
parent 02c775545d
commit 0be0ff963e
7 changed files with 212 additions and 14 deletions

View File

@ -2,7 +2,8 @@
"header": { "header": {
"keySignatures": [], "keySignatures": [],
"meta": [], "meta": [],
"name": "", "name": "Mary Had A Little Lamb",
"artist": "Mrs Mary",
"ppq": 480, "ppq": 480,
"tempos": [ "tempos": [
{ {

View File

@ -28,6 +28,7 @@ struct Track {
struct Header { struct Header {
std::string name; std::string name;
std::string artist;
int ppq; int ppq;
float bpm; float bpm;
}; };
@ -45,7 +46,8 @@ Song parseSong(const rapidjson::Document& doc) {
if (doc.HasMember("header") && doc["header"].IsObject()) { if (doc.HasMember("header") && doc["header"].IsObject()) {
const auto& h = doc["header"].GetObject(); const auto& h = doc["header"].GetObject();
Header header; 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(); header.ppq = h["ppq"].GetInt();
printf("header has tempos: %d\n", h.HasMember("tempos")); printf("header has tempos: %d\n", h.HasMember("tempos"));
if (h.HasMember("tempos") && h["tempos"].IsArray()) { if (h.HasMember("tempos") && h["tempos"].IsArray()) {

View File

@ -1,5 +1,6 @@
#include "samples/ghhb_game.h" #include "samples/ghhb_game.h"
#include "samples/instrument_select.h" #include "samples/instrument_select.h"
#include "samples/song_select.h"
#include "samples/title_screen.h" #include "samples/title_screen.h"
#include "entities/song.h" #include "entities/song.h"
@ -10,6 +11,7 @@
int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1}; int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1};
int INSTRUMENT_PHYSICAL_GAMEPAD[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; Game game;
@ -34,6 +36,7 @@ int main(int argc, char** argv)
font_manager->set_texture_filter("Roboto", TEXTURE_FILTER_BILINEAR); font_manager->set_texture_filter("Roboto", TEXTURE_FILTER_BILINEAR);
game.add_scene<TitleScreen>("title"); game.add_scene<TitleScreen>("title");
game.add_scene<SongSelectScreen>("song_select");
game.add_scene<InstrumentSelectScreen>("instrument_select"); game.add_scene<InstrumentSelectScreen>("instrument_select");
game.add_scene<GHHBScene>("ghhb"); game.add_scene<GHHBScene>("ghhb");

View File

@ -6,6 +6,7 @@
#include "background.h" #include "background.h"
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
#include <string>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
@ -154,10 +155,9 @@ std::vector<Glyph> load_chart(const char* path)
return empty; 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 } // namespace
extern std::string SELECTED_SONG_PATH;
extern int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES]; extern int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES];
extern int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES]; extern int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES];
@ -192,6 +192,14 @@ public:
void on_enter() override 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; song_time = 0.0f;
score = 0; score = 0;
combo = 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>(); background = add_game_object<Background>();
background->add_tag("background"); background->add_tag("background");
} }

View File

@ -79,10 +79,15 @@ public:
int slot = BUTTON_SLOT[b]; int slot = BUTTON_SLOT[b];
int gi = get_or_assign_gamepad_index(gp); int gi = get_or_assign_gamepad_index(gp);
if (gi >= 0) if (gi >= 0)
{
if (instrument_owner[slot] == gi)
instrument_owner[slot] = -1;
else
instrument_owner[slot] = gi; instrument_owner[slot] = gi;
} }
} }
} }
}
bool all_selected = true; bool all_selected = true;
for (int s = 0; s < MAX_INSTRUMENT_TYPES; s++) for (int s = 0; s < MAX_INSTRUMENT_TYPES; s++)

187
src/samples/song_select.h Normal file
View File

@ -0,0 +1,187 @@
#pragma once
#include "engine/prefabs/includes.h"
#include "entities/song.h"
#include "rapidjson/filereadstream.h"
#include <cstdio>
#include <vector>
extern std::string SELECTED_SONG_PATH;
struct SongEntry
{
std::string name;
std::string artist;
std::string path;
};
inline std::vector<SongEntry> build_song_list(const std::vector<std::string>& paths)
{
std::vector<SongEntry> 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<SongEntry> songs;
int selected_index = 0;
int scroll_offset = 0;
void init() override
{
auto font_manager = game->get_manager<FontManager>();
font = font_manager->get_font("Roboto");
std::vector<std::string> 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<int>(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<int>(songs.size()))
{
SELECTED_SONG_PATH = songs[idx].path;
game->go_to_scene("instrument_select");
}
}
}
void draw() override
{
float w = static_cast<float>(GetScreenWidth());
float h = static_cast<float>(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<int>(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);
}
};

View File

@ -18,7 +18,7 @@ public:
// Trigger scene change on Enter key or gamepad start button. // Trigger scene change on Enter key or gamepad start button.
if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT)) if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT))
{ {
game->go_to_scene("instrument_select"); game->go_to_scene("song_select");
} }
} }