Merge branch 'master' of mx.cogentleman.com:cogentleman/guitarHeroButBetter
This commit is contained in:
commit
c54a7a7bd1
@ -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 C4–B4: 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). C4–B4 map to lanes 0–11. 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++)
|
||||||
{
|
{
|
||||||
if (FileExists(path))
|
for (int lane = 0; lane < LANE_COUNT; lane++)
|
||||||
{
|
{
|
||||||
music = LoadMusicStream(path);
|
const char* path = INSTRUMENT_LANE_WAV[slot][lane];
|
||||||
music_loaded = true;
|
if (FileExists(path))
|
||||||
break;
|
{
|
||||||
}
|
note_sounds[slot][lane] = LoadSound(path);
|
||||||
}
|
note_sounds_loaded[slot][lane] = true;
|
||||||
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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user