1078 lines
42 KiB
C++
1078 lines
42 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>
|
|
#include <deque>
|
|
|
|
namespace
|
|
{
|
|
constexpr int LANE_COUNT = 12;
|
|
constexpr int OCTAVE_COUNT = 3;
|
|
constexpr int MIDI_LANE_MIN = 48;
|
|
constexpr int MIDI_LANE_MAX = 83;
|
|
constexpr int MIDI_LANE_COUNT = MIDI_LANE_MAX - MIDI_LANE_MIN + 1;
|
|
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;
|
|
constexpr float GLYPH_HEIGHT_FRACTION_OF_LANE = 0.5f;
|
|
constexpr float MIN_SUSTAIN_FALLBACK_SEC = 0.05f;
|
|
constexpr float MIN_GLYPH_DURATION_SEC = 0.1f;
|
|
|
|
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 float INSTRUMENT_VOLUME[MAX_INSTRUMENT_TYPES] = {
|
|
0.4f,
|
|
0.6f,
|
|
1.0f,
|
|
0.7f
|
|
};
|
|
|
|
/* Index i = MIDI note (48 + i); notes outside [48, 83] are not rendered. */
|
|
const char* const INSTRUMENT_LANE_WAV[MAX_INSTRUMENT_TYPES][MIDI_LANE_COUNT] {
|
|
{ // Instrument 0 - Synth
|
|
"assets/sounds/snes_synth/snes_synth_048.wav", "assets/sounds/snes_synth/snes_synth_049.wav",
|
|
"assets/sounds/snes_synth/snes_synth_050.wav", "assets/sounds/snes_synth/snes_synth_051.wav",
|
|
"assets/sounds/snes_synth/snes_synth_052.wav", "assets/sounds/snes_synth/snes_synth_053.wav",
|
|
"assets/sounds/snes_synth/snes_synth_054.wav", "assets/sounds/snes_synth/snes_synth_055.wav",
|
|
"assets/sounds/snes_synth/snes_synth_056.wav", "assets/sounds/snes_synth/snes_synth_057.wav",
|
|
"assets/sounds/snes_synth/snes_synth_058.wav", "assets/sounds/snes_synth/snes_synth_059.wav",
|
|
"assets/sounds/snes_synth/snes_synth_060.wav", "assets/sounds/snes_synth/snes_synth_061.wav",
|
|
"assets/sounds/snes_synth/snes_synth_062.wav", "assets/sounds/snes_synth/snes_synth_063.wav",
|
|
"assets/sounds/snes_synth/snes_synth_064.wav", "assets/sounds/snes_synth/snes_synth_065.wav",
|
|
"assets/sounds/snes_synth/snes_synth_066.wav", "assets/sounds/snes_synth/snes_synth_067.wav",
|
|
"assets/sounds/snes_synth/snes_synth_068.wav", "assets/sounds/snes_synth/snes_synth_069.wav",
|
|
"assets/sounds/snes_synth/snes_synth_070.wav", "assets/sounds/snes_synth/snes_synth_071.wav",
|
|
"assets/sounds/snes_synth/snes_synth_072.wav", "assets/sounds/snes_synth/snes_synth_073.wav",
|
|
"assets/sounds/snes_synth/snes_synth_074.wav", "assets/sounds/snes_synth/snes_synth_075.wav",
|
|
"assets/sounds/snes_synth/snes_synth_076.wav", "assets/sounds/snes_synth/snes_synth_077.wav",
|
|
"assets/sounds/snes_synth/snes_synth_078.wav", "assets/sounds/snes_synth/snes_synth_079.wav",
|
|
"assets/sounds/snes_synth/snes_synth_080.wav", "assets/sounds/snes_synth/snes_synth_081.wav",
|
|
"assets/sounds/snes_synth/snes_synth_082.wav", "assets/sounds/snes_synth/snes_synth_083.wav"
|
|
},
|
|
{ // Instrument 1 - Sax
|
|
"assets/sounds/bari_sax/bari_sax_048.wav", "assets/sounds/bari_sax/bari_sax_049.wav",
|
|
"assets/sounds/bari_sax/bari_sax_050.wav", "assets/sounds/bari_sax/bari_sax_051.wav",
|
|
"assets/sounds/bari_sax/bari_sax_052.wav", "assets/sounds/bari_sax/bari_sax_053.wav",
|
|
"assets/sounds/bari_sax/bari_sax_054.wav", "assets/sounds/bari_sax/bari_sax_055.wav",
|
|
"assets/sounds/bari_sax/bari_sax_056.wav", "assets/sounds/bari_sax/bari_sax_057.wav",
|
|
"assets/sounds/bari_sax/bari_sax_058.wav", "assets/sounds/bari_sax/bari_sax_059.wav",
|
|
"assets/sounds/bari_sax/bari_sax_060.wav", "assets/sounds/bari_sax/bari_sax_061.wav",
|
|
"assets/sounds/bari_sax/bari_sax_062.wav", "assets/sounds/bari_sax/bari_sax_063.wav",
|
|
"assets/sounds/bari_sax/bari_sax_064.wav", "assets/sounds/bari_sax/bari_sax_065.wav",
|
|
"assets/sounds/bari_sax/bari_sax_066.wav", "assets/sounds/bari_sax/bari_sax_067.wav",
|
|
"assets/sounds/bari_sax/bari_sax_068.wav", "assets/sounds/bari_sax/bari_sax_069.wav",
|
|
"assets/sounds/bari_sax/bari_sax_070.wav", "assets/sounds/bari_sax/bari_sax_071.wav",
|
|
"assets/sounds/bari_sax/bari_sax_072.wav", "assets/sounds/bari_sax/bari_sax_073.wav",
|
|
"assets/sounds/bari_sax/bari_sax_074.wav", "assets/sounds/bari_sax/bari_sax_075.wav",
|
|
"assets/sounds/bari_sax/bari_sax_076.wav", "assets/sounds/bari_sax/bari_sax_077.wav",
|
|
"assets/sounds/bari_sax/bari_sax_078.wav", "assets/sounds/bari_sax/bari_sax_079.wav",
|
|
"assets/sounds/bari_sax/bari_sax_080.wav", "assets/sounds/bari_sax/bari_sax_081.wav",
|
|
"assets/sounds/bari_sax/bari_sax_082.wav", "assets/sounds/bari_sax/bari_sax_083.wav"
|
|
},
|
|
{ // Instrument 2 - Strings
|
|
"assets/sounds/strings/strings_048.wav", "assets/sounds/strings/strings_049.wav",
|
|
"assets/sounds/strings/strings_050.wav", "assets/sounds/strings/strings_051.wav",
|
|
"assets/sounds/strings/strings_052.wav", "assets/sounds/strings/strings_053.wav",
|
|
"assets/sounds/strings/strings_054.wav", "assets/sounds/strings/strings_055.wav",
|
|
"assets/sounds/strings/strings_056.wav", "assets/sounds/strings/strings_057.wav",
|
|
"assets/sounds/strings/strings_058.wav", "assets/sounds/strings/strings_059.wav",
|
|
"assets/sounds/strings/strings_060.wav", "assets/sounds/strings/strings_061.wav",
|
|
"assets/sounds/strings/strings_062.wav", "assets/sounds/strings/strings_063.wav",
|
|
"assets/sounds/strings/strings_064.wav", "assets/sounds/strings/strings_065.wav",
|
|
"assets/sounds/strings/strings_066.wav", "assets/sounds/strings/strings_067.wav",
|
|
"assets/sounds/strings/strings_068.wav", "assets/sounds/strings/strings_069.wav",
|
|
"assets/sounds/strings/strings_070.wav", "assets/sounds/strings/strings_071.wav",
|
|
"assets/sounds/strings/strings_072.wav", "assets/sounds/strings/strings_073.wav",
|
|
"assets/sounds/strings/strings_074.wav", "assets/sounds/strings/strings_075.wav",
|
|
"assets/sounds/strings/strings_076.wav", "assets/sounds/strings/strings_077.wav",
|
|
"assets/sounds/strings/strings_078.wav", "assets/sounds/strings/strings_079.wav",
|
|
"assets/sounds/strings/strings_080.wav", "assets/sounds/strings/strings_081.wav",
|
|
"assets/sounds/strings/strings_082.wav", "assets/sounds/strings/strings_083.wav"
|
|
},
|
|
{ // Instrument 3 - Piano
|
|
"assets/sounds/piano/piano_048.wav", "assets/sounds/piano/piano_049.wav",
|
|
"assets/sounds/piano/piano_050.wav", "assets/sounds/piano/piano_051.wav",
|
|
"assets/sounds/piano/piano_052.wav", "assets/sounds/piano/piano_053.wav",
|
|
"assets/sounds/piano/piano_054.wav", "assets/sounds/piano/piano_055.wav",
|
|
"assets/sounds/piano/piano_056.wav", "assets/sounds/piano/piano_057.wav",
|
|
"assets/sounds/piano/piano_058.wav", "assets/sounds/piano/piano_059.wav",
|
|
"assets/sounds/piano/piano_060.wav", "assets/sounds/piano/piano_061.wav",
|
|
"assets/sounds/piano/piano_062.wav", "assets/sounds/piano/piano_063.wav",
|
|
"assets/sounds/piano/piano_064.wav", "assets/sounds/piano/piano_065.wav",
|
|
"assets/sounds/piano/piano_066.wav", "assets/sounds/piano/piano_067.wav",
|
|
"assets/sounds/piano/piano_068.wav", "assets/sounds/piano/piano_069.wav",
|
|
"assets/sounds/piano/piano_070.wav", "assets/sounds/piano/piano_071.wav",
|
|
"assets/sounds/piano/piano_072.wav", "assets/sounds/piano/piano_073.wav",
|
|
"assets/sounds/piano/piano_074.wav", "assets/sounds/piano/piano_075.wav",
|
|
"assets/sounds/piano/piano_076.wav", "assets/sounds/piano/piano_077.wav",
|
|
"assets/sounds/piano/piano_078.wav", "assets/sounds/piano/piano_079.wav",
|
|
"assets/sounds/piano/piano_080.wav", "assets/sounds/piano/piano_081.wav",
|
|
"assets/sounds/piano/piano_082.wav", "assets/sounds/piano/piano_083.wav"
|
|
}
|
|
};
|
|
|
|
struct Glyph
|
|
{
|
|
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;
|
|
}
|
|
};
|
|
|
|
struct PendingSound
|
|
{
|
|
float play_time = 0.0f;
|
|
float duration_sec = 0.0f;
|
|
int lane = 0;
|
|
int instrument_slot = 0;
|
|
int octave = 0;
|
|
};
|
|
|
|
struct ActiveSustainedSound
|
|
{
|
|
float start_time = 0.0f;
|
|
float end_time = 0.0f;
|
|
int lane = 0;
|
|
int instrument_slot = 0;
|
|
};
|
|
|
|
static size_t pick_track_by_note_count(const Song& song)
|
|
{
|
|
size_t best = 0;
|
|
size_t best_count = 0;
|
|
for (size_t i = 0; i < song.tracks.size(); i++)
|
|
{
|
|
size_t n = song.tracks[i].notes.size();
|
|
if (n > best_count)
|
|
{
|
|
best_count = n;
|
|
best = i;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
std::vector<Glyph> chart_from_song(const Song& song, int track_override)
|
|
{
|
|
std::vector<Glyph> glyphs;
|
|
if (song.tracks.empty())
|
|
return glyphs;
|
|
|
|
size_t track_idx;
|
|
if (track_override >= 0 &&
|
|
static_cast<size_t>(track_override) < song.tracks.size())
|
|
{
|
|
track_idx = static_cast<size_t>(track_override);
|
|
}
|
|
else
|
|
{
|
|
track_idx = pick_track_by_note_count(song);
|
|
}
|
|
const Track& track = song.tracks[track_idx];
|
|
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<int, const Note*>> timed_notes;
|
|
for (const Note& note : track.notes)
|
|
{
|
|
if (note.midi >= MIDI_LANE_MIN && note.midi <= MIDI_LANE_MAX)
|
|
timed_notes.push_back({note.ticks, ¬e});
|
|
}
|
|
std::sort(timed_notes.begin(), timed_notes.end(),
|
|
[](const auto& a, const auto& b) {
|
|
if (a.first != b.first)
|
|
return a.first < b.first;
|
|
return a.second->midi < b.second->midi;
|
|
});
|
|
|
|
int note_index = 0;
|
|
for (const auto& pair : timed_notes)
|
|
{
|
|
const Note& note = *pair.second;
|
|
float time_sec = note.ticks / ticks_per_sec; // This is now the BOTTOM time
|
|
float duration_sec = note.duration_ticks / ticks_per_sec;
|
|
int wav_index = note.midi - MIDI_LANE_MIN;
|
|
int lane = wav_index % LANE_COUNT;
|
|
int octave = wav_index;
|
|
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;
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
if (shortened_duration < MIN_GLYPH_DURATION_SEC)
|
|
{
|
|
note_index++;
|
|
continue;
|
|
}
|
|
|
|
float old_duration = last_in_lane->duration_sec;
|
|
last_in_lane->duration_sec = shortened_duration;
|
|
|
|
}
|
|
}
|
|
|
|
glyphs.push_back(Glyph{time_sec, duration_sec, lane, instrument_slot, octave});
|
|
note_index++;
|
|
}
|
|
|
|
return glyphs;
|
|
}
|
|
|
|
std::vector<Glyph> load_chart(const char* path, int track_override)
|
|
{
|
|
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, track_override);
|
|
}
|
|
std::fclose(fp);
|
|
return empty;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern std::string SELECTED_SONG_PATH;
|
|
extern int SELECTED_TRACK_OVERRIDE;
|
|
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;
|
|
std::unordered_set<Glyph*> missed_notes;
|
|
std::vector<PendingSound> pending_sounds;
|
|
std::vector<ActiveSustainedSound> active_sustained;
|
|
bool instrument_has_hits[MAX_INSTRUMENT_TYPES] = {false};
|
|
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][MIDI_LANE_COUNT] = {{0}};
|
|
bool note_sounds_loaded[MAX_INSTRUMENT_TYPES][MIDI_LANE_COUNT] = {{false}};
|
|
std::deque<Sound> note_sounds_playing[LANE_COUNT][MAX_INSTRUMENT_TYPES];
|
|
static constexpr float PRESS_FLASH_DURATION = 0.12f;
|
|
float press_flash_timer[LANE_COUNT] = {0};
|
|
float hit_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(), SELECTED_TRACK_OVERRIDE);
|
|
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();
|
|
missed_notes.clear();
|
|
pending_sounds.clear();
|
|
active_sustained.clear();
|
|
for (int i = 0; i < MAX_INSTRUMENT_TYPES; i++)
|
|
{
|
|
instrument_has_hits[i] = false;
|
|
}
|
|
for (int i = 0; i < LANE_COUNT; i++)
|
|
{
|
|
press_flash_timer[i] = 0.0f;
|
|
hit_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 < MIDI_LANE_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 < MIDI_LANE_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 is_instrument_auto_played(int instrument_slot) const
|
|
{
|
|
return INSTRUMENT_PHYSICAL_GAMEPAD[instrument_slot] < 0;
|
|
}
|
|
|
|
bool is_lane_held_by_instrument(int lane, int instrument_slot) const
|
|
{
|
|
int physical_id = INSTRUMENT_PHYSICAL_GAMEPAD[instrument_slot];
|
|
if (physical_id < 0)
|
|
return false;
|
|
return IsGamepadAvailable(physical_id) &&
|
|
IsGamepadButtonDown(physical_id, GAMEPAD_BUTTONS[lane]);
|
|
}
|
|
|
|
void stop_playing_released_notes(int lane, float min_sustain_sec)
|
|
{
|
|
for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++)
|
|
{
|
|
if (is_instrument_auto_played(slot))
|
|
continue;
|
|
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();
|
|
}
|
|
active_sustained.erase(it);
|
|
}
|
|
}
|
|
|
|
bool lane_pressed_by_instrument_owner(int lane, int instrument_slot) const
|
|
{
|
|
int physical_id = INSTRUMENT_PHYSICAL_GAMEPAD[instrument_slot];
|
|
if (physical_id < 0)
|
|
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;
|
|
}
|
|
|
|
// 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 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, bool is_player_input = false)
|
|
{
|
|
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;
|
|
completed_notes.insert(n);
|
|
// 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(
|
|
{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)
|
|
{
|
|
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_bottom_hits_line_time = other->time + chart_time_offset;
|
|
if (note_sounds_loaded[other->instrument_slot][other->octave])
|
|
pending_sounds.push_back({other_bottom_hits_line_time, other->duration_sec,
|
|
other->lane, other->instrument_slot, other->octave});
|
|
}
|
|
}
|
|
}
|
|
combo++;
|
|
score += 100 + std::min(combo * 10, 50);
|
|
// Only mark as "has hits" if this was actual player input
|
|
if (is_player_input)
|
|
{
|
|
if (!instrument_has_hits[n->instrument_slot])
|
|
{
|
|
TraceLog(LOG_INFO, "Marking instrument %d as actively played (PHYSICAL_GP=%d)",
|
|
n->instrument_slot, INSTRUMENT_PHYSICAL_GAMEPAD[n->instrument_slot]);
|
|
}
|
|
instrument_has_hits[n->instrument_slot] = true;
|
|
}
|
|
TraceLog(LOG_INFO, "COMBO++ -> %d (lane %d, inst %d, player_input=%d)", combo, n->lane, n->instrument_slot, is_player_input);
|
|
}
|
|
|
|
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();
|
|
{
|
|
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;
|
|
}
|
|
debug_bottom_printed = true;
|
|
}
|
|
}
|
|
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 min_glyph_height_px = lane_width * GLYPH_HEIGHT_FRACTION_OF_LANE;
|
|
float time_per_glyph_height =
|
|
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)
|
|
{
|
|
Sound s = note_sounds[it->instrument_slot][it->octave];
|
|
SetSoundVolume(s, INSTRUMENT_VOLUME[it->instrument_slot]);
|
|
PlaySound(s);
|
|
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, it->play_time + sustain_sec, it->lane, it->instrument_slot});
|
|
it = pending_sounds.erase(it);
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
|
|
for (auto it = active_sustained.begin(); it != active_sustained.end();)
|
|
{
|
|
if (song_time >= it->end_time)
|
|
{
|
|
if (!note_sounds_playing[it->lane][it->instrument_slot].empty())
|
|
{
|
|
StopSound(note_sounds_playing[it->lane][it->instrument_slot].front());
|
|
note_sounds_playing[it->lane][it->instrument_slot].pop_front();
|
|
}
|
|
it = active_sustained.erase(it);
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
|
|
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 (Glyph* n : spawned)
|
|
{
|
|
if (completed_notes.count(n) != 0)
|
|
continue;
|
|
if (missed_notes.count(n) != 0)
|
|
continue;
|
|
bool is_auto = is_instrument_auto_played(n->instrument_slot);
|
|
if (is_auto)
|
|
continue;
|
|
// Only reset combo for instruments that the player is actively playing
|
|
bool has_hits = instrument_has_hits[n->instrument_slot];
|
|
if (!has_hits)
|
|
continue;
|
|
float bottom_y = glyph_bottom_y(*n);
|
|
if (bottom_y > hit_line_y + HIT_ZONE_MARGIN)
|
|
{
|
|
missed_notes.insert(n);
|
|
TraceLog(LOG_WARNING, "COMBO RESET -> 0 (missed note: lane %d, inst %d, is_auto=%d, has_hits=%d, PHYSICAL_GP[%d]=%d)",
|
|
n->lane, n->instrument_slot, is_auto, has_hits, n->instrument_slot, INSTRUMENT_PHYSICAL_GAMEPAD[n->instrument_slot]);
|
|
combo = 0;
|
|
}
|
|
}
|
|
|
|
for (auto it = spawned.begin(); it != spawned.end();)
|
|
{
|
|
Glyph* n = *it;
|
|
float bottom_y = glyph_bottom_y(*n);
|
|
if (bottom_y > screen_height + 40.0f)
|
|
{
|
|
completed_notes.erase(n);
|
|
missed_notes.erase(n);
|
|
it = spawned.erase(it);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
std::vector<Glyph*> to_auto_consume;
|
|
for (Glyph* n : spawned)
|
|
{
|
|
if (completed_notes.count(n) != 0)
|
|
continue;
|
|
if (!is_note_hittable(*n))
|
|
continue;
|
|
if (dev_auto_hit_mode || is_instrument_auto_played(n->instrument_slot))
|
|
to_auto_consume.push_back(n);
|
|
}
|
|
for (Glyph* n : to_auto_consume)
|
|
{
|
|
consume_note(n);
|
|
}
|
|
|
|
for (int lane = 0; lane < LANE_COUNT; 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;
|
|
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 (is_instrument_auto_played(n->instrument_slot))
|
|
continue;
|
|
if (!lane_pressed_by_instrument_owner(lane, n->instrument_slot))
|
|
continue;
|
|
float bottom_y = glyph_bottom_y(*n);
|
|
float d = fabsf(bottom_y - hit_line_y);
|
|
if (d < best_dist)
|
|
{
|
|
best_dist = d;
|
|
best = n;
|
|
}
|
|
}
|
|
if (best != nullptr && pressed)
|
|
{
|
|
consume_note(best, true); // Player input
|
|
}
|
|
else
|
|
{
|
|
TraceLog(LOG_WARNING, "COMBO RESET -> 0 (lane %d pressed but no valid note found)", lane);
|
|
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)});
|
|
}
|
|
}
|
|
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});
|
|
}
|
|
|
|
const Color MISSED_GLYPH_COLOR = {140, 140, 140, 255};
|
|
std::vector<std::vector<Glyph*>> by_lane(static_cast<size_t>(LANE_COUNT));
|
|
for (Glyph* n : spawned)
|
|
{
|
|
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;
|
|
}
|
|
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)];
|
|
// Sort by top Y (which is bottom - height, i.e., earlier in time)
|
|
std::sort(list.begin(), list.end(),
|
|
[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 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<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);
|
|
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 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<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});
|
|
std::string longest_combo_text = "Longest combo: " + std::to_string(longest_combo);
|
|
float longest_combo_w = MeasureTextEx(font, longest_combo_text.c_str(), score_font_size, 1).x;
|
|
DrawTextEx(font, longest_combo_text.c_str(),
|
|
{screen_width / 2.0f - longest_combo_w / 2.0f, screen_height / 2.0f + 10},
|
|
score_font_size, 1, Color{220, 220, 255, 255});
|
|
int max_notes = 0;
|
|
for (int i = 0; i < MAX_GAMEPADS; i++)
|
|
{
|
|
if (notes_hit_by_player[i] > max_notes)
|
|
max_notes = notes_hit_by_player[i];
|
|
}
|
|
if (max_notes > 0)
|
|
{
|
|
std::vector<int> leaders;
|
|
for (int i = 0; i < MAX_GAMEPADS; i++)
|
|
{
|
|
if (notes_hit_by_player[i] == max_notes)
|
|
leaders.push_back(i + 1);
|
|
}
|
|
std::string most_notes_text;
|
|
if (leaders.size() == 1)
|
|
most_notes_text = "Most notes: Player " + std::to_string(leaders[0]) + " (" + std::to_string(max_notes) + ")";
|
|
else
|
|
{
|
|
most_notes_text = "Most notes: Players ";
|
|
for (size_t i = 0; i < leaders.size(); i++)
|
|
{
|
|
if (i > 0)
|
|
most_notes_text += " & ";
|
|
most_notes_text += std::to_string(leaders[i]);
|
|
}
|
|
most_notes_text += " (" + std::to_string(max_notes) + ")";
|
|
}
|
|
float most_notes_w = MeasureTextEx(font, most_notes_text.c_str(), score_font_size, 1).x;
|
|
DrawTextEx(font, most_notes_text.c_str(),
|
|
{screen_width / 2.0f - most_notes_w / 2.0f, screen_height / 2.0f + 40},
|
|
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;
|
|
float prompt_y = (max_notes > 0) ? (screen_height / 2.0f + 70) : (screen_height / 2.0f + 40);
|
|
DrawTextEx(font, prompt,
|
|
{screen_width / 2.0f - prompt_w / 2.0f, prompt_y},
|
|
prompt_font_size, 1, Color{180, 180, 200, 255});
|
|
}
|
|
}
|
|
};
|