diff --git a/src/samples/ghhb_game.h b/src/samples/ghhb_game.h index c5f6ae6..0ff3876 100644 --- a/src/samples/ghhb_game.h +++ b/src/samples/ghhb_game.h @@ -1,20 +1,17 @@ -/** - * DDR / Guitar Hero style rhythm game: 12 lanes, pre-coded notes. - * Supports multiple controllers: any connected gamepad can hit notes (shared chart/score). - * Controller: D-pad (lanes 0-3), face X/Y/A/B (4-7), LB/LT/RB/RT (8-11). 8bitdo compatible. - * Keyboard (Q/W/E/R/A/S/D/F/Z/X/C/V) remains supported. - */ - #pragma once #include "engine/prefabs/includes.h" +#include "entities/song.h" +#include "rapidjson/filereadstream.h" #include +#include #include #include namespace { constexpr int LANE_COUNT = 12; +constexpr int MAX_INSTRUMENT_TYPES = 8; constexpr int MAX_GAMEPADS = 4; constexpr float HIT_WINDOW_PX = 80.0f; constexpr float RECEPTOR_HEIGHT = 100.0f; @@ -50,26 +47,75 @@ const int KEY_KEYS[LANE_COUNT] = { KEY_V, }; -/* Chromatic scale C4–B4: one .wav per lane/button (assets/sounds/nes_harp, copied to build output). */ -const char* const LANE_NOTE_WAV[LANE_COUNT] = { - "assets/sounds/nes_harp/nes_harp_C4.wav", - "assets/sounds/nes_harp/nes_harp_Cs4.wav", - "assets/sounds/nes_harp/nes_harp_D4.wav", - "assets/sounds/nes_harp/nes_harp_Ds4.wav", - "assets/sounds/nes_harp/nes_harp_E4.wav", - "assets/sounds/nes_harp/nes_harp_F4.wav", - "assets/sounds/nes_harp/nes_harp_Fs4.wav", - "assets/sounds/nes_harp/nes_harp_G4.wav", - "assets/sounds/nes_harp/nes_harp_Gs4.wav", - "assets/sounds/nes_harp/nes_harp_A4.wav", - "assets/sounds/nes_harp/nes_harp_As4.wav", - "assets/sounds/nes_harp/nes_harp_B4.wav", +/* One color per instrument type (MIDI program % MAX_INSTRUMENT_TYPES). */ +const Color INSTRUMENT_COLORS[MAX_INSTRUMENT_TYPES] = { + {220, 100, 100, 255}, + {100, 160, 255, 255}, + {100, 220, 120, 255}, + {255, 200, 80, 255}, + {200, 100, 255, 255}, + {80, 255, 220, 255}, + {255, 140, 180, 255}, + {180, 255, 140, 255}, +}; + +/* Per-instrument WAV paths (lane = midi % 12). C4–B4 map to lanes 0–11. Reuse same set until more assets exist. */ +const char* const INSTRUMENT_LANE_WAV[MAX_INSTRUMENT_TYPES][LANE_COUNT] = { + {"assets/sounds/nes_harp/nes_harp_C4.wav", "assets/sounds/nes_harp/nes_harp_Cs4.wav", + "assets/sounds/nes_harp/nes_harp_D4.wav", "assets/sounds/nes_harp/nes_harp_Ds4.wav", + "assets/sounds/nes_harp/nes_harp_E4.wav", "assets/sounds/nes_harp/nes_harp_F4.wav", + "assets/sounds/nes_harp/nes_harp_Fs4.wav", "assets/sounds/nes_harp/nes_harp_G4.wav", + "assets/sounds/nes_harp/nes_harp_Gs4.wav", "assets/sounds/nes_harp/nes_harp_A4.wav", + "assets/sounds/nes_harp/nes_harp_As4.wav", "assets/sounds/nes_harp/nes_harp_B4.wav"}, + {"assets/sounds/nes_harp/nes_harp_C4.wav", "assets/sounds/nes_harp/nes_harp_Cs4.wav", + "assets/sounds/nes_harp/nes_harp_D4.wav", "assets/sounds/nes_harp/nes_harp_Ds4.wav", + "assets/sounds/nes_harp/nes_harp_E4.wav", "assets/sounds/nes_harp/nes_harp_F4.wav", + "assets/sounds/nes_harp/nes_harp_Fs4.wav", "assets/sounds/nes_harp/nes_harp_G4.wav", + "assets/sounds/nes_harp/nes_harp_Gs4.wav", "assets/sounds/nes_harp/nes_harp_A4.wav", + "assets/sounds/nes_harp/nes_harp_As4.wav", "assets/sounds/nes_harp/nes_harp_B4.wav"}, + {"assets/sounds/nes_harp/nes_harp_C4.wav", "assets/sounds/nes_harp/nes_harp_Cs4.wav", + "assets/sounds/nes_harp/nes_harp_D4.wav", "assets/sounds/nes_harp/nes_harp_Ds4.wav", + "assets/sounds/nes_harp/nes_harp_E4.wav", "assets/sounds/nes_harp/nes_harp_F4.wav", + "assets/sounds/nes_harp/nes_harp_Fs4.wav", "assets/sounds/nes_harp/nes_harp_G4.wav", + "assets/sounds/nes_harp/nes_harp_Gs4.wav", "assets/sounds/nes_harp/nes_harp_A4.wav", + "assets/sounds/nes_harp/nes_harp_As4.wav", "assets/sounds/nes_harp/nes_harp_B4.wav"}, + {"assets/sounds/nes_harp/nes_harp_C4.wav", "assets/sounds/nes_harp/nes_harp_Cs4.wav", + "assets/sounds/nes_harp/nes_harp_D4.wav", "assets/sounds/nes_harp/nes_harp_Ds4.wav", + "assets/sounds/nes_harp/nes_harp_E4.wav", "assets/sounds/nes_harp/nes_harp_F4.wav", + "assets/sounds/nes_harp/nes_harp_Fs4.wav", "assets/sounds/nes_harp/nes_harp_G4.wav", + "assets/sounds/nes_harp/nes_harp_Gs4.wav", "assets/sounds/nes_harp/nes_harp_A4.wav", + "assets/sounds/nes_harp/nes_harp_As4.wav", "assets/sounds/nes_harp/nes_harp_B4.wav"}, + {"assets/sounds/nes_harp/nes_harp_C4.wav", "assets/sounds/nes_harp/nes_harp_Cs4.wav", + "assets/sounds/nes_harp/nes_harp_D4.wav", "assets/sounds/nes_harp/nes_harp_Ds4.wav", + "assets/sounds/nes_harp/nes_harp_E4.wav", "assets/sounds/nes_harp/nes_harp_F4.wav", + "assets/sounds/nes_harp/nes_harp_Fs4.wav", "assets/sounds/nes_harp/nes_harp_G4.wav", + "assets/sounds/nes_harp/nes_harp_Gs4.wav", "assets/sounds/nes_harp/nes_harp_A4.wav", + "assets/sounds/nes_harp/nes_harp_As4.wav", "assets/sounds/nes_harp/nes_harp_B4.wav"}, + {"assets/sounds/nes_harp/nes_harp_C4.wav", "assets/sounds/nes_harp/nes_harp_Cs4.wav", + "assets/sounds/nes_harp/nes_harp_D4.wav", "assets/sounds/nes_harp/nes_harp_Ds4.wav", + "assets/sounds/nes_harp/nes_harp_E4.wav", "assets/sounds/nes_harp/nes_harp_F4.wav", + "assets/sounds/nes_harp/nes_harp_Fs4.wav", "assets/sounds/nes_harp/nes_harp_G4.wav", + "assets/sounds/nes_harp/nes_harp_Gs4.wav", "assets/sounds/nes_harp/nes_harp_A4.wav", + "assets/sounds/nes_harp/nes_harp_As4.wav", "assets/sounds/nes_harp/nes_harp_B4.wav"}, + {"assets/sounds/nes_harp/nes_harp_C4.wav", "assets/sounds/nes_harp/nes_harp_Cs4.wav", + "assets/sounds/nes_harp/nes_harp_D4.wav", "assets/sounds/nes_harp/nes_harp_Ds4.wav", + "assets/sounds/nes_harp/nes_harp_E4.wav", "assets/sounds/nes_harp/nes_harp_F4.wav", + "assets/sounds/nes_harp/nes_harp_Fs4.wav", "assets/sounds/nes_harp/nes_harp_G4.wav", + "assets/sounds/nes_harp/nes_harp_Gs4.wav", "assets/sounds/nes_harp/nes_harp_A4.wav", + "assets/sounds/nes_harp/nes_harp_As4.wav", "assets/sounds/nes_harp/nes_harp_B4.wav"}, + {"assets/sounds/nes_harp/nes_harp_C4.wav", "assets/sounds/nes_harp/nes_harp_Cs4.wav", + "assets/sounds/nes_harp/nes_harp_D4.wav", "assets/sounds/nes_harp/nes_harp_Ds4.wav", + "assets/sounds/nes_harp/nes_harp_E4.wav", "assets/sounds/nes_harp/nes_harp_F4.wav", + "assets/sounds/nes_harp/nes_harp_Fs4.wav", "assets/sounds/nes_harp/nes_harp_G4.wav", + "assets/sounds/nes_harp/nes_harp_Gs4.wav", "assets/sounds/nes_harp/nes_harp_A4.wav", + "assets/sounds/nes_harp/nes_harp_As4.wav", "assets/sounds/nes_harp/nes_harp_B4.wav"}, }; struct Glyph { float time = 0.0f; int lane = 0; + int instrument_slot = 0; float y_position(float song_time, float hit_line_y) const { @@ -77,38 +123,58 @@ struct Glyph } }; -std::vector default_chart() +std::vector chart_from_song(const Song& song) { std::vector glyphs; - float t = 2.0f; - for (int i = 0; i < 4; i++) + 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); + + for (const Track& track : song.tracks) { - for (int lane = 0; lane < LANE_COUNT; lane++) + int instrument_slot = track.instrument.number % MAX_INSTRUMENT_TYPES; + if (instrument_slot < 0) + instrument_slot = 0; + for (const Note& note : track.notes) { - glyphs.push_back(Glyph{t, lane}); - t += 0.4f; + if (note.midi < 0 || note.midi > 127) + continue; + float time_sec = note.ticks / ticks_per_sec; + int lane = note.midi % LANE_COUNT; + glyphs.push_back(Glyph{time_sec, lane, instrument_slot}); } - t += 0.6f; - } - for (int lane = 0; lane < LANE_COUNT; lane++) - { - glyphs.push_back(Glyph{t, lane}); - t += 0.2f; - } - t += 0.5f; - for (int i = 0; i < 3; i++) - { - for (int lane = 0; lane < LANE_COUNT; lane++) - { - glyphs.push_back(Glyph{t + lane * 0.08f, lane}); - } - t += 0.8f; } + 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; } -} // namespace -static const char* const GHHB_MUSIC_PATHS[] = {"assets/music/background.ogg", "assets/music/background.mp3"}; +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; +} + +// const char* const GHHB_CHART_PATH = "assets/songs/json/tetris.json"; +const char* const GHHB_CHART_PATH = "assets/songs/json/mary.json"; +} // namespace class GHHBScene : public Scene { @@ -116,7 +182,7 @@ public: Font font = {0}; Music music = {0}; bool music_loaded = false; - std::vector chart = default_chart(); + std::vector chart; std::vector spawned; std::unordered_set completed_notes; float song_time = 0.0f; @@ -126,8 +192,8 @@ public: float lane_width = 0.0f; float screen_width = 0.0f; float screen_height = 0.0f; - Sound note_sounds[LANE_COUNT] = {0}; - bool note_sounds_loaded[LANE_COUNT] = {false}; + Sound note_sounds[MAX_INSTRUMENT_TYPES][LANE_COUNT] = {{0}}; + bool note_sounds_loaded[MAX_INSTRUMENT_TYPES][LANE_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}; @@ -172,11 +238,14 @@ public: StopMusicStream(music); UnloadMusicStream(music); } - for (int i = 0; i < LANE_COUNT; i++) + for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++) { - if (note_sounds_loaded[i]) + for (int lane = 0; lane < LANE_COUNT; lane++) { - UnloadSound(note_sounds[i]); + if (note_sounds_loaded[slot][lane]) + { + UnloadSound(note_sounds[slot][lane]); + } } } } @@ -189,23 +258,19 @@ public: screen_height = static_cast(GetScreenHeight()); hit_line_y = screen_height - RECEPTOR_HEIGHT / 2.0f; lane_width = screen_width / LANE_COUNT; - for (const char* path : GHHB_MUSIC_PATHS) + for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++) { - if (FileExists(path)) + for (int lane = 0; lane < LANE_COUNT; lane++) { - music = LoadMusicStream(path); - music_loaded = true; - break; - } - } - for (int i = 0; i < LANE_COUNT; i++) - { - if (FileExists(LANE_NOTE_WAV[i])) - { - note_sounds[i] = LoadSound(LANE_NOTE_WAV[i]); - note_sounds_loaded[i] = true; + const char* path = INSTRUMENT_LANE_WAV[slot][lane]; + if (FileExists(path)) + { + note_sounds[slot][lane] = LoadSound(path); + note_sounds_loaded[slot][lane] = true; + } } } + chart = load_chart(GHHB_CHART_PATH); } float lane_center_x(int lane) const @@ -259,6 +324,8 @@ public: hit_flash_timer[n->lane] = PRESS_FLASH_DURATION; spawned.erase(it); completed_notes.insert(n); + if (note_sounds_loaded[n->instrument_slot][n->lane]) + PlaySound(note_sounds[n->instrument_slot][n->lane]); } combo++; score += 100 + std::min(combo * 10, 50); @@ -351,13 +418,7 @@ public: } bool pressed = is_lane_pressed(lane); if (pressed) - { press_flash_timer[lane] = PRESS_FLASH_DURATION; - if (note_sounds_loaded[lane]) - { - PlaySound(note_sounds[lane]); - } - } if (!pressed) { continue; @@ -386,6 +447,11 @@ public: { consume_note(best); } + else + { + combo = 0; + score = std::max(0, score - 25); + } } if (is_menu_pressed()) @@ -455,9 +521,12 @@ public: continue; } float cx = lane_center_x(n->lane); - DrawCircle(static_cast(cx), static_cast(y), 22, Color{220, 100, 100, 255}); - DrawCircleLines(static_cast(cx), static_cast(y), 22, - Color{255, 150, 150, 255}); + 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}; + DrawCircle(static_cast(cx), static_cast(y), 22, fill); + DrawCircleLines(static_cast(cx), static_cast(y), 22, edge); } std::string score_text = "Score: " + std::to_string(score) + " Combo: " + std::to_string(combo);