From c370dbef9a53faa5c38fbc0b04b3cc00a1f2197c Mon Sep 17 00:00:00 2001 From: Gordon Weeks <627684+gcweeks@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:30:35 -0800 Subject: [PATCH] Fixed overlapping glyph bug. Reverted mary.json to single track. --- assets/songs/json/mary.json | 307 +++++++++++++++++++++++++++--------- src/entities/song_catalog.h | 6 +- src/samples/ghhb_game.h | 221 ++++++++++++++++++-------- 3 files changed, 393 insertions(+), 141 deletions(-) diff --git a/assets/songs/json/mary.json b/assets/songs/json/mary.json index 0056e54..d346be2 100644 --- a/assets/songs/json/mary.json +++ b/assets/songs/json/mary.json @@ -26,83 +26,246 @@ { "channel": 0, "controlChanges": { - "7": [{"number": 7, "ticks": 0, "time": 0, "value": 0.7874015748031497}] + "1": [ + { + "number": 1, + "ticks": 0, + "time": 0, + "value": 0 + } + ], + "6": [ + { + "number": 6, + "ticks": 0, + "time": 0, + "value": 0.5039370078740157 + }, + { + "number": 6, + "ticks": 0, + "time": 0, + "value": 0.5039370078740157 + }, + { + "number": 6, + "ticks": 0, + "time": 0, + "value": 0.09448818897637795 + } + ], + "7": [ + { + "number": 7, + "ticks": 0, + "time": 0, + "value": 0.7874015748031497 + } + ], + "10": [ + { + "number": 10, + "ticks": 0, + "time": 0, + "value": 0.5039370078740157 + } + ], + "11": [ + { + "number": 11, + "ticks": 0, + "time": 0, + "value": 1 + } + ], + "38": [ + { + "number": 38, + "ticks": 0, + "time": 0, + "value": 0 + } + ], + "100": [ + { + "number": 100, + "ticks": 0, + "time": 0, + "value": 0.015748031496062992 + }, + { + "number": 100, + "ticks": 0, + "time": 0, + "value": 0.007874015748031496 + }, + { + "number": 100, + "ticks": 0, + "time": 0, + "value": 0 + } + ], + "101": [ + { + "number": 101, + "ticks": 0, + "time": 0, + "value": 0 + }, + { + "number": 101, + "ticks": 0, + "time": 0, + "value": 0 + }, + { + "number": 101, + "ticks": 0, + "time": 0, + "value": 0 + } + ], + "121": [ + { + "number": 121, + "ticks": 0, + "time": 0, + "value": 0 + } + ] }, - "pitchBends": [{"ticks": 0, "time": 0, "value": 0}], - "instrument": { - "family": "pipe", - "number": 73, - "name": "flute" - }, - "name": "Melody 1", - "notes": [ - {"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 0, "time": 0, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 480, "time": 0.5, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 60, "name": "C4", "ticks": 960, "time": 1, "velocity": 0.7874015748031497} + "pitchBends": [ + { + "ticks": 0, + "time": 0, + "value": 0 + } ], - "endOfTrackTicks": 1440 - }, - { - "channel": 1, - "controlChanges": { - "7": [{"number": 7, "ticks": 0, "time": 0, "value": 0.7874015748031497}] - }, - "pitchBends": [{"ticks": 0, "time": 0, "value": 0}], "instrument": { - "family": "strings", - "number": 40, - "name": "violin" + "family": "piano", + "number": 0, + "name": "acoustic grand piano" }, - "name": "Melody 2", + "name": "", "notes": [ - {"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 0, "time": 0, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 480, "time": 0.5, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 60, "name": "C4", "ticks": 960, "time": 1, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 1440, "time": 1.5, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 1920, "time": 2, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 2400, "time": 2.5, "velocity": 0.7874015748031497}, - {"duration": 1, "durationTicks": 960, "midi": 64, "name": "E4", "ticks": 2880, "time": 3, "velocity": 0.7874015748031497} - ], - "endOfTrackTicks": 3840 - }, - { - "channel": 2, - "controlChanges": { - "7": [{"number": 7, "ticks": 0, "time": 0, "value": 0.7874015748031497}] - }, - "pitchBends": [{"ticks": 0, "time": 0, "value": 0}], - "instrument": { - "family": "guitar", - "number": 24, - "name": "acoustic guitar (nylon)" - }, - "name": "Melody 3", - "notes": [ - {"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 480, "time": 0.5, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 60, "name": "C4", "ticks": 960, "time": 1, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 3840, "time": 4, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 62, "name": "D4", "ticks": 4320, "time": 4.5, "velocity": 0.7874015748031497}, - {"duration": 1, "durationTicks": 960, "midi": 62, "name": "D4", "ticks": 4800, "time": 5, "velocity": 0.7874015748031497} - ], - "endOfTrackTicks": 5760 - }, - { - "channel": 3, - "controlChanges": { - "7": [{"number": 7, "ticks": 0, "time": 0, "value": 0.7874015748031497}] - }, - "pitchBends": [{"ticks": 0, "time": 0, "value": 0}], - "instrument": { - "family": "chromatic percussion", - "number": 9, - "name": "glockenspiel" - }, - "name": "Melody 4", - "notes": [ - {"duration": 0.5, "durationTicks": 480, "midi": 60, "name": "C4", "ticks": 960, "time": 1, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 64, "name": "E4", "ticks": 5760, "time": 6, "velocity": 0.7874015748031497}, - {"duration": 0.5, "durationTicks": 480, "midi": 67, "name": "G4", "ticks": 6240, "time": 6.5, "velocity": 0.7874015748031497}, - {"duration": 1, "durationTicks": 960, "midi": 67, "name": "G4", "ticks": 6720, "time": 7, "velocity": 0.7874015748031497} + { + "duration": 0.5, + "durationTicks": 480, + "midi": 64, + "name": "E4", + "ticks": 0, + "time": 0, + "velocity": 0.7874015748031497 + }, + { + "duration": 0.5, + "durationTicks": 480, + "midi": 62, + "name": "D4", + "ticks": 480, + "time": 0.5, + "velocity": 0.7874015748031497 + }, + { + "duration": 0.5, + "durationTicks": 480, + "midi": 60, + "name": "C4", + "ticks": 960, + "time": 1, + "velocity": 0.7874015748031497 + }, + { + "duration": 0.5, + "durationTicks": 480, + "midi": 62, + "name": "D4", + "ticks": 1440, + "time": 1.5, + "velocity": 0.7874015748031497 + }, + { + "duration": 0.5, + "durationTicks": 480, + "midi": 64, + "name": "E4", + "ticks": 1920, + "time": 2, + "velocity": 0.7874015748031497 + }, + { + "duration": 0.5, + "durationTicks": 480, + "midi": 64, + "name": "E4", + "ticks": 2400, + "time": 2.5, + "velocity": 0.7874015748031497 + }, + { + "duration": 1, + "durationTicks": 960, + "midi": 64, + "name": "E4", + "ticks": 2880, + "time": 3, + "velocity": 0.7874015748031497 + }, + { + "duration": 0.5, + "durationTicks": 480, + "midi": 62, + "name": "D4", + "ticks": 3840, + "time": 4, + "velocity": 0.7874015748031497 + }, + { + "duration": 0.5, + "durationTicks": 480, + "midi": 62, + "name": "D4", + "ticks": 4320, + "time": 4.5, + "velocity": 0.7874015748031497 + }, + { + "duration": 1, + "durationTicks": 960, + "midi": 62, + "name": "D4", + "ticks": 4800, + "time": 5, + "velocity": 0.7874015748031497 + }, + { + "duration": 0.5, + "durationTicks": 480, + "midi": 64, + "name": "E4", + "ticks": 5760, + "time": 6, + "velocity": 0.7874015748031497 + }, + { + "duration": 0.5, + "durationTicks": 480, + "midi": 67, + "name": "G4", + "ticks": 6240, + "time": 6.5, + "velocity": 0.7874015748031497 + }, + { + "duration": 1, + "durationTicks": 960, + "midi": 67, + "name": "G4", + "ticks": 6720, + "time": 7, + "velocity": 0.7874015748031497 + } ], "endOfTrackTicks": 7680 } diff --git a/src/entities/song_catalog.h b/src/entities/song_catalog.h index a89d18b..b4d23a6 100644 --- a/src/entities/song_catalog.h +++ b/src/entities/song_catalog.h @@ -10,9 +10,9 @@ using SongCatalogEntry = std::pair; inline std::vector get_song_catalog() { return { - {"assets/songs/json/mary.json", -1}, - {"assets/songs/json/pallettown.json", -1}, - {"assets/songs/json/tetris.json", -1}, + {"assets/songs/json/mary.json", 0}, + {"assets/songs/json/pallettown.json", 0}, + {"assets/songs/json/tetris.json", 0}, {"assets/songs/json/undertale.json", 0}, }; } diff --git a/src/samples/ghhb_game.h b/src/samples/ghhb_game.h index af02451..88e97ec 100644 --- a/src/samples/ghhb_game.h +++ b/src/samples/ghhb_game.h @@ -25,6 +25,7 @@ constexpr float SCROLL_PX_PER_SEC = 350.0f; constexpr float LEAD_OFFSET_SECONDS = 3.0f; constexpr float GLYPH_HEIGHT_FRACTION_OF_LANE = 0.5f; constexpr float MIN_SUSTAIN_FALLBACK_SEC = 0.05f; +constexpr float MIN_GLYPH_DURATION_SEC = 0.05f; const int GAMEPAD_BUTTONS[LANE_COUNT] = { GAMEPAD_BUTTON_LEFT_FACE_LEFT, // Left @@ -139,12 +140,13 @@ const char* const INSTRUMENT_LANE_WAV[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_ struct Glyph { - float time = 0.0f; - float duration_sec = 0.0f; + float time = 0.0f; // Time when the BOTTOM of the glyph hits the line (when note should be played) + float duration_sec = 0.0f; // Duration extends upward from bottom (top = time + duration) int lane = 0; int instrument_slot = 0; int octave = 0; + // Returns Y position of the bottom of the glyph float y_position(float song_time, float hit_line_y) const { return hit_line_y - (time - song_time) * SCROLL_PX_PER_SEC; @@ -162,6 +164,7 @@ struct PendingSound struct ActiveSustainedSound { + float start_time = 0.0f; float end_time = 0.0f; int lane = 0; int instrument_slot = 0; @@ -221,12 +224,63 @@ std::vector chart_from_song(const Song& song, int track_override) for (const auto& pair : timed_notes) { const Note& note = *pair.second; - float time_sec = note.ticks / ticks_per_sec; + float time_sec = note.ticks / ticks_per_sec; // This is now the BOTTOM time float duration_sec = note.duration_ticks / ticks_per_sec; int lane = note.midi % LANE_COUNT; int octave = note.midi % (LANE_COUNT * OCTAVE_COUNT); int instrument_slot = note_index % MAX_INSTRUMENT_TYPES; + + // Log original glyph timing (time is bottom, time + duration is top) + float new_bottom_time = time_sec; + float new_top_time = time_sec + duration_sec; + std::printf("[NEW GLYPH] lane=%d bottom=%.3f top=%.3f dur=%.3f\n", + lane, new_bottom_time, new_top_time, duration_sec); + + Glyph* last_in_lane = nullptr; + for (size_t i = glyphs.size(); i-- > 0;) + { + if (glyphs[i].lane == lane) + { + last_in_lane = &glyphs[i]; + break; + } + } + if (last_in_lane != nullptr) + { + // Check if existing glyph's top overlaps with new glyph's bottom + float existing_bottom = last_in_lane->time; + float existing_top = last_in_lane->time + last_in_lane->duration_sec; + + std::printf(" [CHECK] existing lane=%d bottom=%.3f top=%.3f vs new_bottom=%.3f\n", + last_in_lane->lane, existing_bottom, existing_top, new_bottom_time); + + if (existing_top > new_bottom_time) + { + // Truncate existing glyph so its top doesn't go past new glyph's bottom + float shortened_duration = new_bottom_time - existing_bottom; + + std::printf(" [OVERLAP DETECTED] shortened_dur=%.3f (was %.3f)\n", + shortened_duration, last_in_lane->duration_sec); + + if (shortened_duration < MIN_GLYPH_DURATION_SEC) + { + std::printf(" [SKIP] shortened duration %.3f < MIN %.3f, skipping new glyph\n", + shortened_duration, MIN_GLYPH_DURATION_SEC); + note_index++; + continue; + } + + float old_duration = last_in_lane->duration_sec; + last_in_lane->duration_sec = shortened_duration; + + std::printf(" [TRUNCATED] lane=%d: duration %.3f -> %.3f (new_top=%.3f)\n", + last_in_lane->lane, old_duration, shortened_duration, + last_in_lane->time + shortened_duration); + } + } + glyphs.push_back(Glyph{time_sec, duration_sec, lane, instrument_slot, octave}); + std::printf(" [ADDED] glyph count now: %zu\n\n", glyphs.size()); note_index++; } @@ -268,6 +322,7 @@ public: std::vector chart; std::vector spawned; std::unordered_set completed_notes; + std::unordered_set missed_notes; std::vector pending_sounds; std::vector active_sustained; float song_time = 0.0f; @@ -283,10 +338,8 @@ public: bool note_sounds_loaded[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_COUNT] = {{false}}; std::deque note_sounds_playing[LANE_COUNT][MAX_INSTRUMENT_TYPES]; static constexpr float PRESS_FLASH_DURATION = 0.12f; - static constexpr float MISS_FLASH_DURATION = 0.15f; float press_flash_timer[LANE_COUNT] = {0}; float hit_flash_timer[LANE_COUNT] = {0}; - float miss_flash_timer[LANE_COUNT] = {0}; bool game_ended = false; bool dev_auto_hit_mode = false; static constexpr float RESULTS_DELAY_AFTER_LAST_NOTE = 1.0f; @@ -309,13 +362,13 @@ public: dev_auto_hit_mode = false; spawned.clear(); completed_notes.clear(); + missed_notes.clear(); pending_sounds.clear(); active_sustained.clear(); for (int i = 0; i < LANE_COUNT; i++) { press_flash_timer[i] = 0.0f; hit_flash_timer[i] = 0.0f; - miss_flash_timer[i] = 0.0f; } if (music_loaded) { @@ -419,23 +472,26 @@ public: IsGamepadButtonDown(physical_id, GAMEPAD_BUTTONS[lane]); } - void stop_playing_released_notes(int lane) + void stop_playing_released_notes(int lane, float min_sustain_sec) { for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++) { if (is_lane_held_by_instrument(lane, slot)) continue; + auto it = std::find_if(active_sustained.begin(), active_sustained.end(), + [lane, slot](const ActiveSustainedSound& a) { + return a.lane == lane && a.instrument_slot == slot; + }); + if (it == active_sustained.end()) + continue; + if (song_time < it->start_time + min_sustain_sec) + continue; if (!note_sounds_playing[lane][slot].empty()) { StopSound(note_sounds_playing[lane][slot].front()); note_sounds_playing[lane][slot].pop_front(); - auto it = std::find_if(active_sustained.begin(), active_sustained.end(), - [lane, slot](const ActiveSustainedSound& a) { - return a.lane == lane && a.instrument_slot == slot; - }); - if (it != active_sustained.end()) - active_sustained.erase(it); } + active_sustained.erase(it); } } @@ -483,15 +539,24 @@ public: return false; } - float glyph_y(const Glyph& n) const + // Returns the Y position of the BOTTOM of the glyph + // n.time represents when the bottom should reach the hit line + float glyph_bottom_y(const Glyph& n) const { return hit_line_y - (n.time + chart_time_offset - song_time) * SCROLL_PX_PER_SEC; } + static float glyph_height_px(const Glyph& n, float lane_width_val) + { + float min_height = lane_width_val * GLYPH_HEIGHT_FRACTION_OF_LANE; + float duration_height = n.duration_sec * SCROLL_PX_PER_SEC; + return std::max(min_height, duration_height); + } + bool is_note_hittable(const Glyph& n) const { - float y = glyph_y(n); - return y >= upper_bar_y - HIT_ZONE_MARGIN && y <= hit_line_y + HIT_ZONE_MARGIN; + float bottom_y = glyph_bottom_y(n); + return bottom_y >= upper_bar_y - HIT_ZONE_MARGIN && bottom_y <= hit_line_y + HIT_ZONE_MARGIN; } void consume_note(Glyph* n) @@ -500,29 +565,27 @@ public: if (it != spawned.end()) { hit_flash_timer[n->lane] = PRESS_FLASH_DURATION; - spawned.erase(it); completed_notes.insert(n); - float hit_line_time = n->time + chart_time_offset; + // n.time is when the bottom should hit the line + float bottom_hits_line_time = n->time + chart_time_offset; if (note_sounds_loaded[n->instrument_slot][n->octave]) pending_sounds.push_back( - {hit_line_time, n->duration_sec, n->lane, n->instrument_slot, n->octave}); - float y_n = glyph_y(*n); - for (auto it2 = spawned.begin(); it2 != spawned.end();) + {bottom_hits_line_time, n->duration_sec, n->lane, n->instrument_slot, n->octave}); + float bottom_y_n = glyph_bottom_y(*n); + for (Glyph* other : spawned) { - 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) + if (other == n) + continue; + if (other->lane != n->lane || other->instrument_slot != n->instrument_slot) + continue; + float bottom_y_other = glyph_bottom_y(*other); + if (fabsf(bottom_y_other - bottom_y_n) <= SIMULTANEOUS_NOTE_Y_TOLERANCE) { completed_notes.insert(other); - float other_hit_line_time = other->time + chart_time_offset; + float other_bottom_hits_line_time = other->time + chart_time_offset; if (note_sounds_loaded[other->instrument_slot][other->octave]) - pending_sounds.push_back({other_hit_line_time, other->duration_sec, + pending_sounds.push_back({other_bottom_hits_line_time, other->duration_sec, other->lane, other->instrument_slot, other->octave}); - it2 = spawned.erase(it2); - } - else - { - ++it2; } } } @@ -542,6 +605,20 @@ public: void update(float delta_time) override { update_layout(); + { + static bool debug_bottom_printed = false; + if (!debug_bottom_printed && !chart.empty() && lane_width > 0.0f) + { + for (const Glyph& g : chart) + { + float bottom_hits_line_time = g.time + chart_time_offset; + float top_hits_line_time = bottom_hits_line_time + g.duration_sec; + std::printf("[chart] glyph lane=%d bottom_time=%.3f top_time=%.3f dur=%.3f\n", + g.lane, bottom_hits_line_time, top_hits_line_time, g.duration_sec); + } + debug_bottom_printed = true; + } + } if (is_select_pressed()) { dev_auto_hit_mode = !dev_auto_hit_mode; @@ -565,9 +642,9 @@ public: song_time += delta_time; } - float glyph_height_px = lane_width * GLYPH_HEIGHT_FRACTION_OF_LANE; + float min_glyph_height_px = lane_width * GLYPH_HEIGHT_FRACTION_OF_LANE; float time_per_glyph_height = - glyph_height_px > 0.f ? glyph_height_px / SCROLL_PX_PER_SEC : MIN_SUSTAIN_FALLBACK_SEC; + min_glyph_height_px > 0.f ? min_glyph_height_px / SCROLL_PX_PER_SEC : MIN_SUSTAIN_FALLBACK_SEC; for (auto it = pending_sounds.begin(); it != pending_sounds.end();) { if (song_time >= it->play_time) @@ -577,7 +654,7 @@ public: note_sounds_playing[it->lane][it->instrument_slot].push_back(s); float sustain_sec = std::max(it->duration_sec, time_per_glyph_height); active_sustained.push_back( - {it->play_time + sustain_sec, it->lane, it->instrument_slot}); + {it->play_time, it->play_time + sustain_sec, it->lane, it->instrument_slot}); it = pending_sounds.erase(it); } else @@ -634,16 +711,29 @@ public: } } + for (Glyph* n : spawned) + { + if (completed_notes.count(n) != 0) + continue; + if (missed_notes.count(n) != 0) + continue; + float bottom_y = glyph_bottom_y(*n); + if (bottom_y > hit_line_y + HIT_ZONE_MARGIN) + { + missed_notes.insert(n); + combo = 0; + } + } + for (auto it = spawned.begin(); it != spawned.end();) { Glyph* n = *it; - float y = glyph_y(*n); - if (y > hit_line_y + HIT_ZONE_MARGIN) + float bottom_y = glyph_bottom_y(*n); + if (bottom_y > screen_height + 40.0f) { - miss_flash_timer[n->lane] = MISS_FLASH_DURATION; - completed_notes.insert(n); + completed_notes.erase(n); + missed_notes.erase(n); it = spawned.erase(it); - combo = 0; } else { @@ -663,11 +753,6 @@ public: { hit_flash_timer[lane] = 0.0f; } - miss_flash_timer[lane] -= delta_time; - if (miss_flash_timer[lane] < 0.0f) - { - miss_flash_timer[lane] = 0.0f; - } } if (dev_auto_hit_mode) @@ -675,6 +760,8 @@ public: std::vector to_consume; for (Glyph* n : spawned) { + if (completed_notes.count(n) != 0) + continue; if (is_note_hittable(*n)) { to_consume.push_back(n); @@ -689,7 +776,7 @@ public: { for (int lane = 0; lane < LANE_COUNT; lane++) { - stop_playing_released_notes(lane); + stop_playing_released_notes(lane, time_per_glyph_height); bool pressed = is_lane_pressed(lane); if (pressed) press_flash_timer[lane] = PRESS_FLASH_DURATION; @@ -713,8 +800,8 @@ public: { continue; } - float y = glyph_y(*n); - float d = fabsf(y - hit_line_y); + float bottom_y = glyph_bottom_y(*n); + float d = fabsf(bottom_y - hit_line_y); if (d < best_dist) { best_dist = d; @@ -781,15 +868,6 @@ public: static_cast(RECEPTOR_HEIGHT), Color{255, 255, 255, static_cast(alpha)}); } - if (miss_flash_timer[lane] > 0.0f) - { - float alpha = 200.0f * (miss_flash_timer[lane] / MISS_FLASH_DURATION); - DrawRectangle(static_cast(lane * lane_width), - static_cast(upper_bar_y), - static_cast(lane_width), - static_cast(RECEPTOR_HEIGHT), - Color{255, 80, 80, static_cast(alpha)}); - } } DrawLineEx(Vector2{0, upper_bar_y}, Vector2{screen_width, upper_bar_y}, 3.0f, WHITE); DrawLineEx(Vector2{0, hit_line_y}, Vector2{screen_width, hit_line_y}, 3.0f, WHITE); @@ -806,11 +884,14 @@ public: button_label_font_size, 1, Color{220, 220, 240, 255}); } + const Color MISSED_GLYPH_COLOR = {140, 140, 140, 255}; std::vector> by_lane(static_cast(LANE_COUNT)); for (Glyph* n : spawned) { - float y = glyph_y(*n); - if (y < -40.0f || y > screen_height + 40.0f) + float height = glyph_height_px(*n, lane_width); + float bottom = glyph_bottom_y(*n); + float top = bottom - height; + if (bottom < -40.0f || top > screen_height + 40.0f) { continue; } @@ -819,16 +900,23 @@ public: for (int lane = 0; lane < LANE_COUNT; lane++) { std::vector& list = by_lane[static_cast(lane)]; + // Sort by top Y (which is bottom - height, i.e., earlier in time) std::sort(list.begin(), list.end(), - [this](Glyph* a, Glyph* b) { return glyph_y(*a) < glyph_y(*b); }); + [this](Glyph* a, Glyph* b) { + float top_a = glyph_bottom_y(*a) - glyph_height_px(*a, lane_width); + float top_b = glyph_bottom_y(*b) - glyph_height_px(*b, lane_width); + return top_a < top_b; + }); float left_base = lane * lane_width; for (size_t i = 0; i < list.size();) { size_t j = i; - float y0 = glyph_y(*list[i]); - while (j < list.size() && - glyph_y(*list[j]) - y0 <= SIMULTANEOUS_NOTE_Y_TOLERANCE) + float top_y0 = glyph_bottom_y(*list[i]) - glyph_height_px(*list[i], lane_width); + while (j < list.size()) { + float top_y_j = glyph_bottom_y(*list[j]) - glyph_height_px(*list[j], lane_width); + if (top_y_j - top_y0 > SIMULTANEOUS_NOTE_Y_TOLERANCE) + break; j++; } std::vector group(list.begin() + static_cast(i), @@ -844,16 +932,17 @@ public: } } float slice_width = lane_width / static_cast(instrument_count); - float glyph_height = lane_width * GLYPH_HEIGHT_FRACTION_OF_LANE; 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 top = y - glyph_height / 2.0f; - Color fill = INSTRUMENT_COLORS[n->instrument_slot]; + float glyph_height = glyph_height_px(*n, lane_width); + float bottom = glyph_bottom_y(*n); + float top = bottom - glyph_height; + bool missed = missed_notes.count(n) != 0; + Color fill = missed ? MISSED_GLYPH_COLOR : INSTRUMENT_COLORS[n->instrument_slot]; Color edge = Color{ static_cast(std::min(255, fill.r + 35)), static_cast(std::min(255, fill.g + 50)),