guitarHeroButBetter/src/samples/ghhb_game.h
2026-01-31 13:40:40 -08:00

790 lines
30 KiB
C++

#pragma once
#include "engine/prefabs/includes.h"
#include "entities/song.h"
#include "rapidjson/filereadstream.h"
#include "background.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <unordered_set>
#include <vector>
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<Glyph> chart_from_song(const Song& song)
{
std::vector<Glyph> 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<std::pair<size_t, size_t>> 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<size_t>(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<int>(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<Glyph> load_chart(const char* path)
{
std::vector<Glyph> 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<Glyph> chart;
std::vector<Glyph*> spawned;
std::unordered_set<Glyph*> 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> 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<TextureService>();
}
void init() override
{
auto font_manager = game->get_manager<FontManager>();
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>();
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<float>(GetScreenWidth());
screen_height = static_cast<float>(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<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);
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<int>(upper_bar_y),
static_cast<int>(screen_width),
static_cast<int>(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<int>(lane * lane_width),
static_cast<int>(upper_bar_y),
static_cast<int>(lane_width),
static_cast<int>(RECEPTOR_HEIGHT),
Color{80, 255, 120, static_cast<unsigned char>(alpha)});
}
else if (press_flash_timer[lane] > 0.0f)
{
float alpha = 180.0f * (press_flash_timer[lane] / PRESS_FLASH_DURATION);
DrawRectangle(static_cast<int>(lane * lane_width),
static_cast<int>(upper_bar_y),
static_cast<int>(lane_width),
static_cast<int>(RECEPTOR_HEIGHT),
Color{255, 255, 255, static_cast<unsigned char>(alpha)});
}
if (miss_flash_timer[lane] > 0.0f)
{
float alpha = 200.0f * (miss_flash_timer[lane] / MISS_FLASH_DURATION);
DrawRectangle(static_cast<int>(lane * lane_width),
static_cast<int>(upper_bar_y),
static_cast<int>(lane_width),
static_cast<int>(RECEPTOR_HEIGHT),
Color{255, 80, 80, static_cast<unsigned char>(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<std::vector<Glyph*>> by_lane(static_cast<size_t>(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<size_t>(n->lane)].push_back(n);
}
for (int lane = 0; lane < LANE_COUNT; lane++)
{
std::vector<Glyph*>& list = by_lane[static_cast<size_t>(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<Glyph*> group(list.begin() + static_cast<std::ptrdiff_t>(i),
list.begin() + static_cast<std::ptrdiff_t>(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<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 top = y - glyph_height / 2.0f;
Color fill = INSTRUMENT_COLORS[n->instrument_slot];
Color edge = Color{
static_cast<unsigned char>(std::min(255, fill.r + 35)),
static_cast<unsigned char>(std::min(255, fill.g + 50)),
static_cast<unsigned char>(std::min(255, fill.b + 50)), 255};
DrawRectangle(static_cast<int>(left), static_cast<int>(top),
static_cast<int>(slice_width),
static_cast<int>(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<int>(screen_width), static_cast<int>(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});
}
}
};