Added instruments

This commit is contained in:
Gordon Weeks 2026-01-31 02:43:18 -08:00
parent d1a2ac6ab9
commit f9ce562044

View File

@ -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 #pragma once
#include "engine/prefabs/includes.h" #include "engine/prefabs/includes.h"
#include "entities/song.h"
#include "rapidjson/filereadstream.h"
#include <algorithm> #include <algorithm>
#include <cstdio>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
namespace namespace
{ {
constexpr int LANE_COUNT = 12; constexpr int LANE_COUNT = 12;
constexpr int MAX_INSTRUMENT_TYPES = 8;
constexpr int MAX_GAMEPADS = 4; constexpr int MAX_GAMEPADS = 4;
constexpr float HIT_WINDOW_PX = 80.0f; constexpr float HIT_WINDOW_PX = 80.0f;
constexpr float RECEPTOR_HEIGHT = 100.0f; constexpr float RECEPTOR_HEIGHT = 100.0f;
@ -50,26 +47,75 @@ const int KEY_KEYS[LANE_COUNT] = {
KEY_V, KEY_V,
}; };
/* Chromatic scale C4B4: one .wav per lane/button (assets/sounds/nes_harp, copied to build output). */ /* One color per instrument type (MIDI program % MAX_INSTRUMENT_TYPES). */
const char* const LANE_NOTE_WAV[LANE_COUNT] = { const Color INSTRUMENT_COLORS[MAX_INSTRUMENT_TYPES] = {
"assets/sounds/nes_harp/nes_harp_C4.wav", {220, 100, 100, 255},
"assets/sounds/nes_harp/nes_harp_Cs4.wav", {100, 160, 255, 255},
"assets/sounds/nes_harp/nes_harp_D4.wav", {100, 220, 120, 255},
"assets/sounds/nes_harp/nes_harp_Ds4.wav", {255, 200, 80, 255},
"assets/sounds/nes_harp/nes_harp_E4.wav", {200, 100, 255, 255},
"assets/sounds/nes_harp/nes_harp_F4.wav", {80, 255, 220, 255},
"assets/sounds/nes_harp/nes_harp_Fs4.wav", {255, 140, 180, 255},
"assets/sounds/nes_harp/nes_harp_G4.wav", {180, 255, 140, 255},
"assets/sounds/nes_harp/nes_harp_Gs4.wav", };
"assets/sounds/nes_harp/nes_harp_A4.wav",
"assets/sounds/nes_harp/nes_harp_As4.wav", /* Per-instrument WAV paths (lane = midi % 12). C4B4 map to lanes 011. Reuse same set until more assets exist. */
"assets/sounds/nes_harp/nes_harp_B4.wav", 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 struct Glyph
{ {
float time = 0.0f; float time = 0.0f;
int lane = 0; int lane = 0;
int instrument_slot = 0;
float y_position(float song_time, float hit_line_y) const float y_position(float song_time, float hit_line_y) const
{ {
@ -77,38 +123,58 @@ struct Glyph
} }
}; };
std::vector<Glyph> default_chart() std::vector<Glyph> chart_from_song(const Song& song)
{ {
std::vector<Glyph> glyphs; std::vector<Glyph> glyphs;
float t = 2.0f; int ppq = song.header.ppq > 0 ? song.header.ppq : 480;
for (int i = 0; i < 4; i++) 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}); if (note.midi < 0 || note.midi > 127)
t += 0.4f; 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; return glyphs;
} }
} // namespace
static const char* const GHHB_MUSIC_PATHS[] = {"assets/music/background.ogg", "assets/music/background.mp3"}; 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;
}
// 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 class GHHBScene : public Scene
{ {
@ -116,7 +182,7 @@ public:
Font font = {0}; Font font = {0};
Music music = {0}; Music music = {0};
bool music_loaded = false; bool music_loaded = false;
std::vector<Glyph> chart = default_chart(); std::vector<Glyph> chart;
std::vector<Glyph*> spawned; std::vector<Glyph*> spawned;
std::unordered_set<Glyph*> completed_notes; std::unordered_set<Glyph*> completed_notes;
float song_time = 0.0f; float song_time = 0.0f;
@ -126,8 +192,8 @@ public:
float lane_width = 0.0f; float lane_width = 0.0f;
float screen_width = 0.0f; float screen_width = 0.0f;
float screen_height = 0.0f; float screen_height = 0.0f;
Sound note_sounds[LANE_COUNT] = {0}; Sound note_sounds[MAX_INSTRUMENT_TYPES][LANE_COUNT] = {{0}};
bool note_sounds_loaded[LANE_COUNT] = {false}; bool note_sounds_loaded[MAX_INSTRUMENT_TYPES][LANE_COUNT] = {{false}};
static constexpr float PRESS_FLASH_DURATION = 0.12f; static constexpr float PRESS_FLASH_DURATION = 0.12f;
static constexpr float MISS_FLASH_DURATION = 0.15f; static constexpr float MISS_FLASH_DURATION = 0.15f;
float press_flash_timer[LANE_COUNT] = {0}; float press_flash_timer[LANE_COUNT] = {0};
@ -172,11 +238,14 @@ public:
StopMusicStream(music); StopMusicStream(music);
UnloadMusicStream(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<float>(GetScreenHeight()); screen_height = static_cast<float>(GetScreenHeight());
hit_line_y = screen_height - RECEPTOR_HEIGHT / 2.0f; hit_line_y = screen_height - RECEPTOR_HEIGHT / 2.0f;
lane_width = screen_width / LANE_COUNT; lane_width = screen_width / LANE_COUNT;
for (const char* path : GHHB_MUSIC_PATHS) for (int slot = 0; slot < MAX_INSTRUMENT_TYPES; slot++)
{ {
for (int lane = 0; lane < LANE_COUNT; lane++)
{
const char* path = INSTRUMENT_LANE_WAV[slot][lane];
if (FileExists(path)) if (FileExists(path))
{ {
music = LoadMusicStream(path); note_sounds[slot][lane] = LoadSound(path);
music_loaded = true; note_sounds_loaded[slot][lane] = 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;
}
} }
chart = load_chart(GHHB_CHART_PATH);
} }
float lane_center_x(int lane) const float lane_center_x(int lane) const
@ -259,6 +324,8 @@ public:
hit_flash_timer[n->lane] = PRESS_FLASH_DURATION; hit_flash_timer[n->lane] = PRESS_FLASH_DURATION;
spawned.erase(it); spawned.erase(it);
completed_notes.insert(n); completed_notes.insert(n);
if (note_sounds_loaded[n->instrument_slot][n->lane])
PlaySound(note_sounds[n->instrument_slot][n->lane]);
} }
combo++; combo++;
score += 100 + std::min(combo * 10, 50); score += 100 + std::min(combo * 10, 50);
@ -351,13 +418,7 @@ public:
} }
bool pressed = is_lane_pressed(lane); bool pressed = is_lane_pressed(lane);
if (pressed) if (pressed)
{
press_flash_timer[lane] = PRESS_FLASH_DURATION; press_flash_timer[lane] = PRESS_FLASH_DURATION;
if (note_sounds_loaded[lane])
{
PlaySound(note_sounds[lane]);
}
}
if (!pressed) if (!pressed)
{ {
continue; continue;
@ -386,6 +447,11 @@ public:
{ {
consume_note(best); consume_note(best);
} }
else
{
combo = 0;
score = std::max(0, score - 25);
}
} }
if (is_menu_pressed()) if (is_menu_pressed())
@ -455,9 +521,12 @@ public:
continue; continue;
} }
float cx = lane_center_x(n->lane); float cx = lane_center_x(n->lane);
DrawCircle(static_cast<int>(cx), static_cast<int>(y), 22, Color{220, 100, 100, 255}); Color fill = INSTRUMENT_COLORS[n->instrument_slot];
DrawCircleLines(static_cast<int>(cx), static_cast<int>(y), 22, Color edge = Color{static_cast<unsigned char>(std::min(255, fill.r + 35)),
Color{255, 150, 150, 255}); static_cast<unsigned char>(std::min(255, fill.g + 50)),
static_cast<unsigned char>(std::min(255, fill.b + 50)), 255};
DrawCircle(static_cast<int>(cx), static_cast<int>(y), 22, fill);
DrawCircleLines(static_cast<int>(cx), static_cast<int>(y), 22, edge);
} }
std::string score_text = "Score: " + std::to_string(score) + " Combo: " + std::to_string(combo); std::string score_text = "Score: " + std::to_string(score) + " Combo: " + std::to_string(combo);