Implemented song catalog feature. Added undertale and pallet town to song list. Revised logic for multiple simultaneous notes.

This commit is contained in:
Gordon Weeks 2026-01-31 13:11:44 -08:00
parent edc9e1dbb3
commit 3bf5456921
8 changed files with 80039 additions and 20 deletions

View File

@ -2,8 +2,8 @@
"header": { "header": {
"keySignatures": [], "keySignatures": [],
"meta": [], "meta": [],
"name": "Mary Had A Little Lamb", "name": "Mary Had a Lil' Lamb",
"artist": "Mrs Mary", "artist": "Mary J. Blige",
"ppq": 480, "ppq": 480,
"tempos": [ "tempos": [
{ {

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@
} }
], ],
"name": "Tetris Theme", "name": "Tetris Theme",
"artist": "Ivan the Terrible",
"ppq": 120, "ppq": 120,
"tempos": [ "tempos": [
{ {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
#pragma once
#include <string>
#include <vector>
inline std::vector<std::string> get_song_catalog()
{
return {
"assets/songs/json/mary.json",
"assets/songs/json/pallettown.json",
"assets/songs/json/tetris.json",
"assets/songs/json/undertale.json",
};
}
inline std::string get_default_song_path()
{
auto catalog = get_song_catalog();
return catalog.empty() ? "" : catalog.front();
}

View File

@ -3,6 +3,7 @@
#include "samples/song_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"
#include "entities/song_catalog.h"
// Emscripten is used for web builds. // Emscripten is used for web builds.
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
@ -11,7 +12,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"; std::string SELECTED_SONG_PATH = get_default_song_path();
Game game; Game game;
@ -40,10 +41,20 @@ int main(int argc, char** argv)
game.add_scene<InstrumentSelectScreen>("instrument_select"); game.add_scene<InstrumentSelectScreen>("instrument_select");
game.add_scene<GHHBScene>("ghhb"); game.add_scene<GHHBScene>("ghhb");
Song& song = song_manager->load_song("mary_had_a_lil_lamb", "assets/songs/json/mary.json"); auto catalog = get_song_catalog();
printf("Song name: %s\n", song.header.name.c_str()); for (const auto& path : catalog)
printf("Song bpm: %f\n", song.header.bpm); {
printf("First note duration: %d\n", song.tracks[0].notes[0].duration_ms); song_manager->load_song(path, path);
}
std::string default_path = get_default_song_path();
Song& song = song_manager->get_song(default_path);
if (!default_path.empty() && song_manager->has_song(default_path))
{
printf("Song name: %s\n", song.header.name.c_str());
printf("Song bpm: %f\n", song.header.bpm);
if (!song.tracks.empty() && !song.tracks[0].notes.empty())
printf("First note duration: %d\n", song.tracks[0].notes[0].duration_ms);
}
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__

View File

@ -5,6 +5,7 @@
#include "rapidjson/filereadstream.h" #include "rapidjson/filereadstream.h"
#include "background.h" #include "background.h"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <cstdio> #include <cstdio>
#include <string> #include <string>
#include <unordered_set> #include <unordered_set>
@ -41,6 +42,16 @@ const char* const GAMEPAD_BUTTON_LABELS[LANE_COUNT] = {
"<", "^", ">", "v", "X", "Y", "B", "A", "LT", "LB", "RB", "RT", "<", "^", ">", "v", "X", "Y", "B", "A", "LT", "LB", "RB", "RT",
}; };
constexpr int COMBO_TIER_COUNT = 20;
constexpr int COMBO_DISPLAY_THRESHOLD = 5;
constexpr int COMBO_NOTES_PER_TIER = 10;
const char* const COMBO_TIER_LABELS[COMBO_TIER_COUNT] = {
"Cool!", "Awesome!", "Radical!", "Amazing!", "Incredible!",
"Fantastic!", "Excellent!", "Outstanding!", "Perfect!", "Brilliant!",
"Superb!", "Magnificent!", "Phenomenal!", "Splendid!", "Terrific!",
"Stellar!", "Legendary!", "Epic!", "Divine!", "Godlike!",
};
const int KEY_KEYS[LANE_COUNT] = { const int KEY_KEYS[LANE_COUNT] = {
KEY_Q, KEY_Q,
KEY_W, KEY_W,
@ -143,11 +154,23 @@ std::vector<Glyph> chart_from_song(const Song& song)
float bpm = song.header.bpm > 0.0f ? song.header.bpm : 120.0f; float bpm = song.header.bpm > 0.0f ? song.header.bpm : 120.0f;
float ticks_per_sec = ppq * (bpm / 60.0f); float ticks_per_sec = ppq * (bpm / 60.0f);
int track_index = 0; std::vector<std::pair<size_t, size_t>> track_note_counts;
for (const Track& track : song.tracks) for (size_t i = 0; i < song.tracks.size(); i++)
track_note_counts.push_back({i, song.tracks[i].notes.size()});
std::sort(track_note_counts.begin(), track_note_counts.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
size_t n_used = std::min(static_cast<size_t>(MAX_INSTRUMENT_TYPES), track_note_counts.size());
for (size_t slot = 0; slot < n_used; slot++)
{ {
int instrument_slot = track_index % MAX_INSTRUMENT_TYPES; size_t track_idx = track_note_counts[slot].first;
track_index++; size_t note_count = track_note_counts[slot].second;
const Track& track = song.tracks[track_idx];
std::printf("Instrument %zu: \"%s\" (family=%s number=%d) %zu notes\n",
slot, track.name.c_str(), track.instrument.family.c_str(),
track.instrument.number, note_count);
int instrument_slot = static_cast<int>(slot);
for (const Note& note : track.notes) for (const Note& note : track.notes)
{ {
if (note.midi < 0 || note.midi > 127) if (note.midi < 0 || note.midi > 127)
@ -218,6 +241,7 @@ public:
float hit_flash_timer[LANE_COUNT] = {0}; float hit_flash_timer[LANE_COUNT] = {0};
float miss_flash_timer[LANE_COUNT] = {0}; float miss_flash_timer[LANE_COUNT] = {0};
bool game_ended = false; bool game_ended = false;
bool dev_auto_hit_mode = false;
static constexpr float RESULTS_DELAY_AFTER_LAST_NOTE = 1.0f; static constexpr float RESULTS_DELAY_AFTER_LAST_NOTE = 1.0f;
std::shared_ptr<Background> background; std::shared_ptr<Background> background;
@ -235,6 +259,7 @@ public:
score = 0; score = 0;
combo = 0; combo = 0;
game_ended = false; game_ended = false;
dev_auto_hit_mode = false;
spawned.clear(); spawned.clear();
completed_notes.clear(); completed_notes.clear();
for (int i = 0; i < LANE_COUNT; i++) for (int i = 0; i < LANE_COUNT; i++)
@ -359,6 +384,18 @@ public:
return false; return false;
} }
bool is_select_pressed() const
{
for (int i = 0; i < MAX_GAMEPADS; i++)
{
if (IsGamepadAvailable(i) && IsGamepadButtonPressed(i, GAMEPAD_BUTTON_MIDDLE_LEFT))
{
return true;
}
}
return false;
}
float glyph_y(const Glyph& n) const float glyph_y(const Glyph& n) const
{ {
return hit_line_y - (n.time + chart_time_offset - song_time) * SCROLL_PX_PER_SEC; return hit_line_y - (n.time + chart_time_offset - song_time) * SCROLL_PX_PER_SEC;
@ -381,6 +418,21 @@ public:
printf("note lane: %d, note octave: %d\n", n->lane, n->octave); printf("note lane: %d, note octave: %d\n", n->lane, n->octave);
if (note_sounds_loaded[n->instrument_slot][n->octave]) if (note_sounds_loaded[n->instrument_slot][n->octave])
PlaySound(note_sounds[n->instrument_slot][n->octave]); PlaySound(note_sounds[n->instrument_slot][n->octave]);
float y_n = glyph_y(*n);
for (auto it2 = spawned.begin(); it2 != spawned.end();)
{
Glyph* other = *it2;
if (other != n && other->lane == n->lane && other->instrument_slot == n->instrument_slot &&
fabsf(glyph_y(*other) - y_n) <= SIMULTANEOUS_NOTE_Y_TOLERANCE)
{
completed_notes.insert(other);
it2 = spawned.erase(it2);
}
else
{
++it2;
}
}
} }
combo++; combo++;
score += 100 + std::min(combo * 10, 50); score += 100 + std::min(combo * 10, 50);
@ -398,6 +450,10 @@ public:
void update(float delta_time) override void update(float delta_time) override
{ {
update_layout(); update_layout();
if (is_select_pressed())
{
dev_auto_hit_mode = !dev_auto_hit_mode;
}
if (game_ended) if (game_ended)
{ {
if (is_menu_pressed()) if (is_menu_pressed())
@ -482,6 +538,27 @@ public:
{ {
miss_flash_timer[lane] = 0.0f; miss_flash_timer[lane] = 0.0f;
} }
}
if (dev_auto_hit_mode)
{
std::vector<Glyph*> to_consume;
for (Glyph* n : spawned)
{
if (is_note_hittable(*n))
{
to_consume.push_back(n);
}
}
for (Glyph* n : to_consume)
{
consume_note(n);
}
}
else
{
for (int lane = 0; lane < LANE_COUNT; lane++)
{
bool pressed = is_lane_pressed(lane); bool pressed = is_lane_pressed(lane);
if (pressed) if (pressed)
press_flash_timer[lane] = PRESS_FLASH_DURATION; press_flash_timer[lane] = PRESS_FLASH_DURATION;
@ -523,6 +600,7 @@ public:
score = std::max(0, score - 25); score = std::max(0, score - 25);
} }
} }
}
if (is_menu_pressed()) if (is_menu_pressed())
{ {
@ -622,14 +700,27 @@ public:
{ {
j++; j++;
} }
int group_count = static_cast<int>(j - i); std::vector<Glyph*> group(list.begin() + static_cast<std::ptrdiff_t>(i),
float slice_width = lane_width / static_cast<float>(group_count); list.begin() + static_cast<std::ptrdiff_t>(j));
float glyph_height = lane_width / 2.0f; std::sort(group.begin(), group.end(),
for (int k = 0; k < group_count; k++) [](Glyph* a, Glyph* b) { return a->instrument_slot < b->instrument_slot; });
int instrument_count = 0;
for (size_t g = 0; g < group.size(); g++)
{ {
Glyph* n = list[i + static_cast<size_t>(k)]; if (g == 0 || group[g]->instrument_slot != group[g - 1]->instrument_slot)
{
instrument_count++;
}
}
float slice_width = lane_width / static_cast<float>(instrument_count);
float glyph_height = lane_width / 2.0f;
int column = 0;
for (size_t g = 0; g < group.size();)
{
int slot = group[g]->instrument_slot;
float left = left_base + column * slice_width;
Glyph* n = group[g];
float y = glyph_y(*n); float y = glyph_y(*n);
float left = left_base + k * slice_width;
float top = y - glyph_height / 2.0f; float top = y - glyph_height / 2.0f;
Color fill = INSTRUMENT_COLORS[n->instrument_slot]; Color fill = INSTRUMENT_COLORS[n->instrument_slot];
Color edge = Color{ Color edge = Color{
@ -641,13 +732,33 @@ public:
static_cast<int>(glyph_height), fill); static_cast<int>(glyph_height), fill);
DrawRectangleLinesEx( DrawRectangleLinesEx(
Rectangle{left, top, slice_width, glyph_height}, 2.0f, edge); Rectangle{left, top, slice_width, glyph_height}, 2.0f, edge);
while (g < group.size() && group[g]->instrument_slot == slot)
{
g++;
}
column++;
} }
i = j; i = j;
} }
} }
std::string score_text = "Score: " + std::to_string(score) + " Combo: " + std::to_string(combo); std::string score_text = "Score: " + std::to_string(score);
DrawTextEx(font, score_text.c_str(), {20, 16}, 28, 1, WHITE); DrawTextEx(font, score_text.c_str(), {20, 16}, 28, 1, WHITE);
if (combo >= COMBO_DISPLAY_THRESHOLD)
{
int tier_index = std::min(COMBO_TIER_COUNT - 1,
(combo - COMBO_DISPLAY_THRESHOLD) / COMBO_NOTES_PER_TIER);
int size_tier = (combo - COMBO_DISPLAY_THRESHOLD) / COMBO_NOTES_PER_TIER;
std::string combo_popup = std::string(COMBO_TIER_LABELS[tier_index]) + " " + std::to_string(combo);
const float combo_font_size = 72.0f + size_tier * 6.0f;
float combo_w = MeasureTextEx(font, combo_popup.c_str(), combo_font_size, 1).x;
const float combo_margin = 24.0f;
DrawTextEx(font, combo_popup.c_str(),
{screen_width - combo_w - combo_margin, combo_margin},
combo_font_size, 1, Color{255, 220, 100, 255});
}
DrawTextEx(font, "Arrows / D-pad / X Y A B / LB LT RB RT: hit when note is between the two white lines", DrawTextEx(font, "Arrows / D-pad / X Y A B / LB LT RB RT: hit when note is between the two white lines",
{20, upper_bar_y - 28}, 18, 1, Color{200, 200, 200, 255}); {20, upper_bar_y - 28}, 18, 1, Color{200, 200, 200, 255});

View File

@ -2,6 +2,7 @@
#include "engine/prefabs/includes.h" #include "engine/prefabs/includes.h"
#include "entities/song.h" #include "entities/song.h"
#include "entities/song_catalog.h"
#include "rapidjson/filereadstream.h" #include "rapidjson/filereadstream.h"
#include <cstdio> #include <cstdio>
#include <vector> #include <vector>
@ -58,8 +59,7 @@ public:
{ {
auto font_manager = game->get_manager<FontManager>(); auto font_manager = game->get_manager<FontManager>();
font = font_manager->get_font("Roboto"); font = font_manager->get_font("Roboto");
std::vector<std::string> paths = {"assets/songs/json/mary.json"}; songs = build_song_list(get_song_catalog());
songs = build_song_list(paths);
selected_index = 0; selected_index = 0;
scroll_offset = 0; scroll_offset = 0;
} }