#pragma once #include "engine/prefabs/includes.h" #include "entities/song.h" #include "rapidjson/filereadstream.h" #include "background.h" #include #include #include #include #include #include namespace { constexpr int LANE_COUNT = 12; constexpr int OCTAVE_COUNT = 2; constexpr int MAX_GAMEPADS = 4; constexpr int MAX_INSTRUMENT_TYPES = MAX_GAMEPADS; constexpr float RECEPTOR_HEIGHT = 150.0f; constexpr float HIT_ZONE_MARGIN = 20.0f; constexpr float SIMULTANEOUS_NOTE_Y_TOLERANCE = 2.0f; constexpr float SCROLL_PX_PER_SEC = 350.0f; constexpr float LEAD_OFFSET_SECONDS = 3.0f; const int GAMEPAD_BUTTONS[LANE_COUNT] = { GAMEPAD_BUTTON_LEFT_FACE_LEFT, // Left GAMEPAD_BUTTON_LEFT_FACE_UP, // Up GAMEPAD_BUTTON_LEFT_FACE_RIGHT, // Right GAMEPAD_BUTTON_LEFT_FACE_DOWN, // Down GAMEPAD_BUTTON_RIGHT_FACE_LEFT, // X GAMEPAD_BUTTON_RIGHT_FACE_UP, // Y GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, // B GAMEPAD_BUTTON_RIGHT_FACE_DOWN, // A GAMEPAD_BUTTON_LEFT_TRIGGER_2, // LT GAMEPAD_BUTTON_LEFT_TRIGGER_1, // LB GAMEPAD_BUTTON_RIGHT_TRIGGER_1, // RB GAMEPAD_BUTTON_RIGHT_TRIGGER_2, // RT }; const char* const GAMEPAD_BUTTON_LABELS[LANE_COUNT] = { "<", "^", ">", "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] = { KEY_Q, KEY_W, KEY_E, KEY_R, KEY_A, KEY_S, KEY_D, KEY_F, KEY_Z, KEY_X, KEY_C, KEY_V, }; /* One color per track (track index % MAX_INSTRUMENT_TYPES). */ const Color INSTRUMENT_COLORS[MAX_INSTRUMENT_TYPES] = { {220, 100, 100, 255}, // red {100, 160, 255, 255}, // blue {100, 220, 120, 255}, // green {255, 200, 80, 255}, // yellow }; const char* const INSTRUMENT_LANE_WAV[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_COUNT] { { // Instrument 0 "assets/sounds/genesis_bass/genesis_bass_024.wav", "assets/sounds/genesis_bass/genesis_bass_025.wav", "assets/sounds/genesis_bass/genesis_bass_026.wav", "assets/sounds/genesis_bass/genesis_bass_027.wav", "assets/sounds/genesis_bass/genesis_bass_028.wav", "assets/sounds/genesis_bass/genesis_bass_029.wav", "assets/sounds/genesis_bass/genesis_bass_030.wav", "assets/sounds/genesis_bass/genesis_bass_031.wav", "assets/sounds/genesis_bass/genesis_bass_032.wav", "assets/sounds/genesis_bass/genesis_bass_033.wav", "assets/sounds/genesis_bass/genesis_bass_034.wav", "assets/sounds/genesis_bass/genesis_bass_035.wav", "assets/sounds/genesis_bass/genesis_bass_036.wav", "assets/sounds/genesis_bass/genesis_bass_037.wav", "assets/sounds/genesis_bass/genesis_bass_038.wav", "assets/sounds/genesis_bass/genesis_bass_039.wav", "assets/sounds/genesis_bass/genesis_bass_040.wav", "assets/sounds/genesis_bass/genesis_bass_041.wav", "assets/sounds/genesis_bass/genesis_bass_042.wav", "assets/sounds/genesis_bass/genesis_bass_043.wav", "assets/sounds/genesis_bass/genesis_bass_044.wav", "assets/sounds/genesis_bass/genesis_bass_045.wav", "assets/sounds/genesis_bass/genesis_bass_046.wav", "assets/sounds/genesis_bass/genesis_bass_047.wav" }, { // Instrument 1 "assets/sounds/genesis_bass/genesis_bass_024.wav", "assets/sounds/genesis_bass/genesis_bass_025.wav", "assets/sounds/genesis_bass/genesis_bass_026.wav", "assets/sounds/genesis_bass/genesis_bass_027.wav", "assets/sounds/genesis_bass/genesis_bass_028.wav", "assets/sounds/genesis_bass/genesis_bass_029.wav", "assets/sounds/genesis_bass/genesis_bass_030.wav", "assets/sounds/genesis_bass/genesis_bass_031.wav", "assets/sounds/genesis_bass/genesis_bass_032.wav", "assets/sounds/genesis_bass/genesis_bass_033.wav", "assets/sounds/genesis_bass/genesis_bass_034.wav", "assets/sounds/genesis_bass/genesis_bass_035.wav", "assets/sounds/genesis_bass/genesis_bass_036.wav", "assets/sounds/genesis_bass/genesis_bass_037.wav", "assets/sounds/genesis_bass/genesis_bass_038.wav", "assets/sounds/genesis_bass/genesis_bass_039.wav", "assets/sounds/genesis_bass/genesis_bass_040.wav", "assets/sounds/genesis_bass/genesis_bass_041.wav", "assets/sounds/genesis_bass/genesis_bass_042.wav", "assets/sounds/genesis_bass/genesis_bass_043.wav", "assets/sounds/genesis_bass/genesis_bass_044.wav", "assets/sounds/genesis_bass/genesis_bass_045.wav", "assets/sounds/genesis_bass/genesis_bass_046.wav", "assets/sounds/genesis_bass/genesis_bass_047.wav" }, { // Instrument 2 "assets/sounds/genesis_bass/genesis_bass_024.wav", "assets/sounds/genesis_bass/genesis_bass_025.wav", "assets/sounds/genesis_bass/genesis_bass_026.wav", "assets/sounds/genesis_bass/genesis_bass_027.wav", "assets/sounds/genesis_bass/genesis_bass_028.wav", "assets/sounds/genesis_bass/genesis_bass_029.wav", "assets/sounds/genesis_bass/genesis_bass_030.wav", "assets/sounds/genesis_bass/genesis_bass_031.wav", "assets/sounds/genesis_bass/genesis_bass_032.wav", "assets/sounds/genesis_bass/genesis_bass_033.wav", "assets/sounds/genesis_bass/genesis_bass_034.wav", "assets/sounds/genesis_bass/genesis_bass_035.wav", "assets/sounds/genesis_bass/genesis_bass_036.wav", "assets/sounds/genesis_bass/genesis_bass_037.wav", "assets/sounds/genesis_bass/genesis_bass_038.wav", "assets/sounds/genesis_bass/genesis_bass_039.wav", "assets/sounds/genesis_bass/genesis_bass_040.wav", "assets/sounds/genesis_bass/genesis_bass_041.wav", "assets/sounds/genesis_bass/genesis_bass_042.wav", "assets/sounds/genesis_bass/genesis_bass_043.wav", "assets/sounds/genesis_bass/genesis_bass_044.wav", "assets/sounds/genesis_bass/genesis_bass_045.wav", "assets/sounds/genesis_bass/genesis_bass_046.wav", "assets/sounds/genesis_bass/genesis_bass_047.wav" }, { // Instrument 3 "assets/sounds/genesis_bass/genesis_bass_024.wav", "assets/sounds/genesis_bass/genesis_bass_025.wav", "assets/sounds/genesis_bass/genesis_bass_026.wav", "assets/sounds/genesis_bass/genesis_bass_027.wav", "assets/sounds/genesis_bass/genesis_bass_028.wav", "assets/sounds/genesis_bass/genesis_bass_029.wav", "assets/sounds/genesis_bass/genesis_bass_030.wav", "assets/sounds/genesis_bass/genesis_bass_031.wav", "assets/sounds/genesis_bass/genesis_bass_032.wav", "assets/sounds/genesis_bass/genesis_bass_033.wav", "assets/sounds/genesis_bass/genesis_bass_034.wav", "assets/sounds/genesis_bass/genesis_bass_035.wav", "assets/sounds/genesis_bass/genesis_bass_036.wav", "assets/sounds/genesis_bass/genesis_bass_037.wav", "assets/sounds/genesis_bass/genesis_bass_038.wav", "assets/sounds/genesis_bass/genesis_bass_039.wav", "assets/sounds/genesis_bass/genesis_bass_040.wav", "assets/sounds/genesis_bass/genesis_bass_041.wav", "assets/sounds/genesis_bass/genesis_bass_042.wav", "assets/sounds/genesis_bass/genesis_bass_043.wav", "assets/sounds/genesis_bass/genesis_bass_044.wav", "assets/sounds/genesis_bass/genesis_bass_045.wav", "assets/sounds/genesis_bass/genesis_bass_046.wav", "assets/sounds/genesis_bass/genesis_bass_047.wav" } }; struct Glyph { float time = 0.0f; int lane = 0; int instrument_slot = 0; int octave = 0; float y_position(float song_time, float hit_line_y) const { return hit_line_y - (time - song_time) * SCROLL_PX_PER_SEC; } }; std::vector chart_from_song(const Song& song) { std::vector glyphs; int ppq = song.header.ppq > 0 ? song.header.ppq : 480; float bpm = song.header.bpm > 0.0f ? song.header.bpm : 120.0f; float ticks_per_sec = ppq * (bpm / 60.0f); std::vector> track_note_counts; 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(MAX_INSTRUMENT_TYPES), track_note_counts.size()); for (size_t slot = 0; slot < n_used; slot++) { size_t track_idx = track_note_counts[slot].first; 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(slot); for (const Note& note : track.notes) { if (note.midi < 0 || note.midi > 127) continue; float time_sec = note.ticks / ticks_per_sec; int lane = note.midi % LANE_COUNT; int octave = note.midi % (LANE_COUNT * OCTAVE_COUNT); glyphs.push_back(Glyph{time_sec, lane, instrument_slot, octave}); } } std::sort(glyphs.begin(), glyphs.end(), [](const Glyph& a, const Glyph& b) { if (a.time != b.time) return a.time < b.time; return a.lane < b.lane; }); return glyphs; } std::vector load_chart(const char* path) { std::vector empty; std::FILE* fp = std::fopen(path, "rb"); if (!fp) return empty; char read_buf[65536]; rapidjson::FileReadStream is(fp, read_buf, sizeof(read_buf)); rapidjson::Document doc; if (!doc.ParseStream(is).HasParseError()) { Song song = parseSong(doc); std::fclose(fp); return chart_from_song(song); } std::fclose(fp); return empty; } } // namespace extern std::string SELECTED_SONG_PATH; extern int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES]; extern int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES]; class GHHBScene : public Scene { public: Font font = {0}; Music music = {0}; bool music_loaded = false; std::vector chart; std::vector spawned; std::unordered_set completed_notes; float song_time = 0.0f; float chart_time_offset = 0.0f; int score = 0; int combo = 0; float upper_bar_y = 0.0f; float hit_line_y = 0.0f; float lane_width = 0.0f; float screen_width = 0.0f; float screen_height = 0.0f; Sound note_sounds[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_COUNT] = {{0}}; bool note_sounds_loaded[MAX_INSTRUMENT_TYPES][LANE_COUNT * OCTAVE_COUNT] = {{false}}; 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; std::shared_ptr background; 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; game_ended = false; dev_auto_hit_mode = false; spawned.clear(); completed_notes.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) { StopMusicStream(music); PlayMusicStream(music); if (chart_time_offset < 0.0f) { SeekMusicStream(music, -chart_time_offset); } } } void on_exit() override { if (music_loaded) { StopMusicStream(music); } } ~GHHBScene() override { if (music_loaded) { StopMusicStream(music); UnloadMusicStream(music); } for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++) { for (int lane = 0; lane < LANE_COUNT * OCTAVE_COUNT; lane++) { if (note_sounds_loaded[slot][lane]) { UnloadSound(note_sounds[slot][lane]); } } } } void init_services() override { add_service(); } void init() override { auto font_manager = game->get_manager(); font = font_manager->get_font("Roboto"); update_layout(); for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++) { for (int lane = 0; lane < LANE_COUNT * OCTAVE_COUNT; lane++) { const char* path = INSTRUMENT_LANE_WAV[slot][lane]; if (FileExists(path)) { note_sounds[slot][lane] = LoadSound(path); note_sounds_loaded[slot][lane] = true; } } } background = add_game_object(); background->add_tag("background"); } float lane_center_x(int lane) const { return (lane + 0.5f) * lane_width; } bool is_lane_pressed(int lane) const { if (IsKeyPressed(KEY_KEYS[lane])) { return true; } for (int i = 0; i < MAX_GAMEPADS; i++) { if (IsGamepadAvailable(i) && IsGamepadButtonPressed(i, GAMEPAD_BUTTONS[lane])) { return true; } } return false; } bool lane_pressed_by_instrument_owner(int lane, int instrument_slot) const { int physical_id = INSTRUMENT_PHYSICAL_GAMEPAD[instrument_slot]; if (physical_id < 0) { for (int i = 0; i < MAX_GAMEPADS; i++) { if (IsGamepadAvailable(i) && IsGamepadButtonPressed(i, GAMEPAD_BUTTONS[lane])) return true; } return false; } return IsGamepadAvailable(physical_id) && IsGamepadButtonPressed(physical_id, GAMEPAD_BUTTONS[lane]); } bool is_menu_pressed() const { if (IsKeyPressed(KEY_ENTER)) { return true; } for (int i = 0; i < MAX_GAMEPADS; i++) { if (IsGamepadAvailable(i) && IsGamepadButtonPressed(i, GAMEPAD_BUTTON_MIDDLE_RIGHT)) { return true; } } 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 { return hit_line_y - (n.time + chart_time_offset - song_time) * SCROLL_PX_PER_SEC; } 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; } void consume_note(Glyph* n) { auto it = std::find_if(spawned.begin(), spawned.end(), [n](Glyph* p) { return p == n; }); if (it != spawned.end()) { hit_flash_timer[n->lane] = PRESS_FLASH_DURATION; spawned.erase(it); completed_notes.insert(n); printf("note lane: %d, note octave: %d\n", n->lane, n->octave); if (note_sounds_loaded[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++; score += 100 + std::min(combo * 10, 50); } void update_layout() { screen_width = static_cast(GetScreenWidth()); screen_height = static_cast(GetScreenHeight()); upper_bar_y = screen_height - RECEPTOR_HEIGHT; hit_line_y = screen_height - RECEPTOR_HEIGHT / 2.0f; lane_width = screen_width / LANE_COUNT; } void update(float delta_time) override { update_layout(); if (is_select_pressed()) { dev_auto_hit_mode = !dev_auto_hit_mode; } if (game_ended) { if (is_menu_pressed()) { game->go_to_scene("title"); } return; } if (music_loaded) { UpdateMusicStream(music); song_time = GetMusicTimePlayed(music); } else { song_time += delta_time; } float last_note_time = 0.0f; for (const auto& n : chart) { float t = n.time + chart_time_offset; if (t > last_note_time) { last_note_time = t; } } float note_exit_seconds = (hit_line_y - upper_bar_y) / SCROLL_PX_PER_SEC; if (song_time >= last_note_time + note_exit_seconds + RESULTS_DELAY_AFTER_LAST_NOTE) { game_ended = true; return; } float lead_seconds = (hit_line_y - 60.0f) / SCROLL_PX_PER_SEC; for (auto& n : chart) { if (completed_notes.count(&n) != 0) { continue; } bool already = std::find_if(spawned.begin(), spawned.end(), [&n](Glyph* p) { return p == &n; }) != spawned.end(); if (!already && song_time >= n.time + chart_time_offset - lead_seconds) { spawned.push_back(&n); } } for (auto it = spawned.begin(); it != spawned.end();) { Glyph* n = *it; float y = glyph_y(*n); if (y > hit_line_y + HIT_ZONE_MARGIN) { miss_flash_timer[n->lane] = MISS_FLASH_DURATION; completed_notes.insert(n); it = spawned.erase(it); combo = 0; } else { ++it; } } for (int lane = 0; lane < LANE_COUNT; lane++) { press_flash_timer[lane] -= delta_time; if (press_flash_timer[lane] < 0.0f) { press_flash_timer[lane] = 0.0f; } hit_flash_timer[lane] -= delta_time; if (hit_flash_timer[lane] < 0.0f) { 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) { std::vector 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); if (pressed) press_flash_timer[lane] = PRESS_FLASH_DURATION; if (!pressed) { continue; } Glyph* best = nullptr; float best_dist = 1e9f; for (Glyph* n : spawned) { if (n->lane != lane) { continue; } if (!is_note_hittable(*n)) { continue; } if (!lane_pressed_by_instrument_owner(lane, n->instrument_slot)) { continue; } float y = glyph_y(*n); float d = fabsf(y - hit_line_y); if (d < best_dist) { best_dist = d; best = n; } } if (best != nullptr) { consume_note(best); } else { combo = 0; score = std::max(0, score - 25); } } } if (is_menu_pressed()) { game->go_to_scene("title"); } } void draw_scene() override { if (background) { background->draw_object(); } draw(); } void draw() override { for (int lane = 0; lane < LANE_COUNT; lane++) { float cx = lane_center_x(lane); DrawLineEx(Vector2{cx, 0}, Vector2{cx, screen_height}, 2.0f, Color{70, 70, 90, 255}); } DrawRectangle(0, static_cast(upper_bar_y), static_cast(screen_width), static_cast(RECEPTOR_HEIGHT), Color{60, 60, 100, 255}); for (int lane = 0; lane < LANE_COUNT; lane++) { if (hit_flash_timer[lane] > 0.0f) { float alpha = 180.0f * (hit_flash_timer[lane] / PRESS_FLASH_DURATION); DrawRectangle(static_cast(lane * lane_width), static_cast(upper_bar_y), static_cast(lane_width), static_cast(RECEPTOR_HEIGHT), Color{80, 255, 120, static_cast(alpha)}); } else if (press_flash_timer[lane] > 0.0f) { float alpha = 180.0f * (press_flash_timer[lane] / PRESS_FLASH_DURATION); DrawRectangle(static_cast(lane * lane_width), static_cast(upper_bar_y), static_cast(lane_width), 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); const float button_label_font_size = 28.0f; const float button_label_y = hit_line_y + 18.0f; for (int lane = 0; lane < LANE_COUNT; lane++) { const char* label = GAMEPAD_BUTTON_LABELS[lane]; float label_w = MeasureTextEx(font, label, button_label_font_size, 1).x; float cx = lane_center_x(lane); DrawTextEx(font, label, {cx - label_w / 2.0f, button_label_y}, button_label_font_size, 1, Color{220, 220, 240, 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) { continue; } by_lane[static_cast(n->lane)].push_back(n); } for (int lane = 0; lane < LANE_COUNT; lane++) { std::vector& list = by_lane[static_cast(lane)]; std::sort(list.begin(), list.end(), [this](Glyph* a, Glyph* b) { return glyph_y(*a) < glyph_y(*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) { j++; } std::vector group(list.begin() + static_cast(i), list.begin() + static_cast(j)); std::sort(group.begin(), group.end(), [](Glyph* a, Glyph* b) { return a->instrument_slot < b->instrument_slot; }); int instrument_count = 0; for (size_t g = 0; g < group.size(); g++) { if (g == 0 || group[g]->instrument_slot != group[g - 1]->instrument_slot) { instrument_count++; } } float slice_width = lane_width / static_cast(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 top = y - glyph_height / 2.0f; Color fill = INSTRUMENT_COLORS[n->instrument_slot]; Color edge = Color{ static_cast(std::min(255, fill.r + 35)), static_cast(std::min(255, fill.g + 50)), static_cast(std::min(255, fill.b + 50)), 255}; DrawRectangle(static_cast(left), static_cast(top), static_cast(slice_width), static_cast(glyph_height), fill); DrawRectangleLinesEx( Rectangle{left, top, slice_width, glyph_height}, 2.0f, edge); while (g < group.size() && group[g]->instrument_slot == slot) { g++; } column++; } i = j; } } std::string score_text = "Score: " + std::to_string(score); 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", {20, upper_bar_y - 28}, 18, 1, Color{200, 200, 200, 255}); if (game_ended) { DrawRectangle(0, 0, static_cast(screen_width), static_cast(screen_height), Color{0, 0, 0, 200}); const char* title_text = "Song complete!"; const float title_font_size = 42.0f; float title_w = MeasureTextEx(font, title_text, title_font_size, 1).x; DrawTextEx(font, title_text, {screen_width / 2.0f - title_w / 2.0f, screen_height / 2.0f - 80}, title_font_size, 1, WHITE); std::string final_score_text = "Final score: " + std::to_string(score); const float score_font_size = 32.0f; float score_w = MeasureTextEx(font, final_score_text.c_str(), score_font_size, 1).x; DrawTextEx(font, final_score_text.c_str(), {screen_width / 2.0f - score_w / 2.0f, screen_height / 2.0f - 20}, score_font_size, 1, Color{220, 220, 255, 255}); const char* prompt = "Press Enter to return to menu"; const float prompt_font_size = 20.0f; float prompt_w = MeasureTextEx(font, prompt, prompt_font_size, 1).x; DrawTextEx(font, prompt, {screen_width / 2.0f - prompt_w / 2.0f, screen_height / 2.0f + 40}, prompt_font_size, 1, Color{180, 180, 200, 255}); } } };