Implemented song catalog feature. Added undertale and pallet town to song list. Revised logic for multiple simultaneous notes.
This commit is contained in:
parent
edc9e1dbb3
commit
3bf5456921
@ -2,8 +2,8 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"keySignatures": [],
|
"keySignatures": [],
|
||||||
"meta": [],
|
"meta": [],
|
||||||
"name": "Mary Had A Little Lamb",
|
"name": "Mary Had a Lil' Lamb",
|
||||||
"artist": "Mrs Mary",
|
"artist": "Mary J. Blige",
|
||||||
"ppq": 480,
|
"ppq": 480,
|
||||||
"tempos": [
|
"tempos": [
|
||||||
{
|
{
|
||||||
|
|||||||
6211
assets/songs/json/pallettown.json
Normal file
6211
assets/songs/json/pallettown.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"name": "Tetris Theme",
|
"name": "Tetris Theme",
|
||||||
|
"artist": "Ivan the Terrible",
|
||||||
"ppq": 120,
|
"ppq": 120,
|
||||||
"tempos": [
|
"tempos": [
|
||||||
{
|
{
|
||||||
|
|||||||
73665
assets/songs/json/undertale.json
Normal file
73665
assets/songs/json/undertale.json
Normal file
File diff suppressed because it is too large
Load Diff
20
src/entities/song_catalog.h
Normal file
20
src/entities/song_catalog.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
inline std::vector<std::string> get_song_catalog()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
"assets/songs/json/mary.json",
|
||||||
|
"assets/songs/json/pallettown.json",
|
||||||
|
"assets/songs/json/tetris.json",
|
||||||
|
"assets/songs/json/undertale.json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string get_default_song_path()
|
||||||
|
{
|
||||||
|
auto catalog = get_song_catalog();
|
||||||
|
return catalog.empty() ? "" : catalog.front();
|
||||||
|
}
|
||||||
15
src/main.cpp
15
src/main.cpp
@ -3,6 +3,7 @@
|
|||||||
#include "samples/song_select.h"
|
#include "samples/song_select.h"
|
||||||
#include "samples/title_screen.h"
|
#include "samples/title_screen.h"
|
||||||
#include "entities/song.h"
|
#include "entities/song.h"
|
||||||
|
#include "entities/song_catalog.h"
|
||||||
|
|
||||||
// Emscripten is used for web builds.
|
// Emscripten is used for web builds.
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
@ -11,7 +12,7 @@
|
|||||||
|
|
||||||
int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1};
|
int INSTRUMENT_GAMEPAD_INDEX[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1};
|
||||||
int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1};
|
int INSTRUMENT_PHYSICAL_GAMEPAD[MAX_INSTRUMENT_TYPES] = {-1, -1, -1, -1};
|
||||||
std::string SELECTED_SONG_PATH = "assets/songs/json/mary.json";
|
std::string SELECTED_SONG_PATH = get_default_song_path();
|
||||||
|
|
||||||
Game game;
|
Game game;
|
||||||
|
|
||||||
@ -40,10 +41,20 @@ int main(int argc, char** argv)
|
|||||||
game.add_scene<InstrumentSelectScreen>("instrument_select");
|
game.add_scene<InstrumentSelectScreen>("instrument_select");
|
||||||
game.add_scene<GHHBScene>("ghhb");
|
game.add_scene<GHHBScene>("ghhb");
|
||||||
|
|
||||||
Song& song = song_manager->load_song("mary_had_a_lil_lamb", "assets/songs/json/mary.json");
|
auto catalog = get_song_catalog();
|
||||||
|
for (const auto& path : catalog)
|
||||||
|
{
|
||||||
|
song_manager->load_song(path, path);
|
||||||
|
}
|
||||||
|
std::string default_path = get_default_song_path();
|
||||||
|
Song& song = song_manager->get_song(default_path);
|
||||||
|
if (!default_path.empty() && song_manager->has_song(default_path))
|
||||||
|
{
|
||||||
printf("Song name: %s\n", song.header.name.c_str());
|
printf("Song name: %s\n", song.header.name.c_str());
|
||||||
printf("Song bpm: %f\n", song.header.bpm);
|
printf("Song bpm: %f\n", song.header.bpm);
|
||||||
|
if (!song.tracks.empty() && !song.tracks[0].notes.empty())
|
||||||
printf("First note duration: %d\n", song.tracks[0].notes[0].duration_ms);
|
printf("First note duration: %d\n", song.tracks[0].notes[0].duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include "rapidjson/filereadstream.h"
|
#include "rapidjson/filereadstream.h"
|
||||||
#include "background.h"
|
#include "background.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
@ -41,6 +42,16 @@ const char* const GAMEPAD_BUTTON_LABELS[LANE_COUNT] = {
|
|||||||
"<", "^", ">", "v", "X", "Y", "B", "A", "LT", "LB", "RB", "RT",
|
"<", "^", ">", "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] = {
|
const int KEY_KEYS[LANE_COUNT] = {
|
||||||
KEY_Q,
|
KEY_Q,
|
||||||
KEY_W,
|
KEY_W,
|
||||||
@ -143,11 +154,23 @@ std::vector<Glyph> chart_from_song(const Song& song)
|
|||||||
float bpm = song.header.bpm > 0.0f ? song.header.bpm : 120.0f;
|
float bpm = song.header.bpm > 0.0f ? song.header.bpm : 120.0f;
|
||||||
float ticks_per_sec = ppq * (bpm / 60.0f);
|
float ticks_per_sec = ppq * (bpm / 60.0f);
|
||||||
|
|
||||||
int track_index = 0;
|
std::vector<std::pair<size_t, size_t>> track_note_counts;
|
||||||
for (const Track& track : song.tracks)
|
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++)
|
||||||
{
|
{
|
||||||
int instrument_slot = track_index % MAX_INSTRUMENT_TYPES;
|
size_t track_idx = track_note_counts[slot].first;
|
||||||
track_index++;
|
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)
|
for (const Note& note : track.notes)
|
||||||
{
|
{
|
||||||
if (note.midi < 0 || note.midi > 127)
|
if (note.midi < 0 || note.midi > 127)
|
||||||
@ -218,6 +241,7 @@ public:
|
|||||||
float hit_flash_timer[LANE_COUNT] = {0};
|
float hit_flash_timer[LANE_COUNT] = {0};
|
||||||
float miss_flash_timer[LANE_COUNT] = {0};
|
float miss_flash_timer[LANE_COUNT] = {0};
|
||||||
bool game_ended = false;
|
bool game_ended = false;
|
||||||
|
bool dev_auto_hit_mode = false;
|
||||||
static constexpr float RESULTS_DELAY_AFTER_LAST_NOTE = 1.0f;
|
static constexpr float RESULTS_DELAY_AFTER_LAST_NOTE = 1.0f;
|
||||||
std::shared_ptr<Background> background;
|
std::shared_ptr<Background> background;
|
||||||
|
|
||||||
@ -235,6 +259,7 @@ public:
|
|||||||
score = 0;
|
score = 0;
|
||||||
combo = 0;
|
combo = 0;
|
||||||
game_ended = false;
|
game_ended = false;
|
||||||
|
dev_auto_hit_mode = false;
|
||||||
spawned.clear();
|
spawned.clear();
|
||||||
completed_notes.clear();
|
completed_notes.clear();
|
||||||
for (int i = 0; i < LANE_COUNT; i++)
|
for (int i = 0; i < LANE_COUNT; i++)
|
||||||
@ -359,6 +384,18 @@ public:
|
|||||||
return false;
|
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
|
float glyph_y(const Glyph& n) const
|
||||||
{
|
{
|
||||||
return hit_line_y - (n.time + chart_time_offset - song_time) * SCROLL_PX_PER_SEC;
|
return hit_line_y - (n.time + chart_time_offset - song_time) * SCROLL_PX_PER_SEC;
|
||||||
@ -381,6 +418,21 @@ public:
|
|||||||
printf("note lane: %d, note octave: %d\n", n->lane, n->octave);
|
printf("note lane: %d, note octave: %d\n", n->lane, n->octave);
|
||||||
if (note_sounds_loaded[n->instrument_slot][n->octave])
|
if (note_sounds_loaded[n->instrument_slot][n->octave])
|
||||||
PlaySound(note_sounds[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++;
|
combo++;
|
||||||
score += 100 + std::min(combo * 10, 50);
|
score += 100 + std::min(combo * 10, 50);
|
||||||
@ -398,6 +450,10 @@ public:
|
|||||||
void update(float delta_time) override
|
void update(float delta_time) override
|
||||||
{
|
{
|
||||||
update_layout();
|
update_layout();
|
||||||
|
if (is_select_pressed())
|
||||||
|
{
|
||||||
|
dev_auto_hit_mode = !dev_auto_hit_mode;
|
||||||
|
}
|
||||||
if (game_ended)
|
if (game_ended)
|
||||||
{
|
{
|
||||||
if (is_menu_pressed())
|
if (is_menu_pressed())
|
||||||
@ -482,6 +538,27 @@ public:
|
|||||||
{
|
{
|
||||||
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);
|
bool pressed = is_lane_pressed(lane);
|
||||||
if (pressed)
|
if (pressed)
|
||||||
press_flash_timer[lane] = PRESS_FLASH_DURATION;
|
press_flash_timer[lane] = PRESS_FLASH_DURATION;
|
||||||
@ -523,6 +600,7 @@ public:
|
|||||||
score = std::max(0, score - 25);
|
score = std::max(0, score - 25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (is_menu_pressed())
|
if (is_menu_pressed())
|
||||||
{
|
{
|
||||||
@ -622,14 +700,27 @@ public:
|
|||||||
{
|
{
|
||||||
j++;
|
j++;
|
||||||
}
|
}
|
||||||
int group_count = static_cast<int>(j - i);
|
std::vector<Glyph*> group(list.begin() + static_cast<std::ptrdiff_t>(i),
|
||||||
float slice_width = lane_width / static_cast<float>(group_count);
|
list.begin() + static_cast<std::ptrdiff_t>(j));
|
||||||
float glyph_height = lane_width / 2.0f;
|
std::sort(group.begin(), group.end(),
|
||||||
for (int k = 0; k < group_count; k++)
|
[](Glyph* a, Glyph* b) { return a->instrument_slot < b->instrument_slot; });
|
||||||
|
int instrument_count = 0;
|
||||||
|
for (size_t g = 0; g < group.size(); g++)
|
||||||
{
|
{
|
||||||
Glyph* n = list[i + static_cast<size_t>(k)];
|
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 y = glyph_y(*n);
|
||||||
float left = left_base + k * slice_width;
|
|
||||||
float top = y - glyph_height / 2.0f;
|
float top = y - glyph_height / 2.0f;
|
||||||
Color fill = INSTRUMENT_COLORS[n->instrument_slot];
|
Color fill = INSTRUMENT_COLORS[n->instrument_slot];
|
||||||
Color edge = Color{
|
Color edge = Color{
|
||||||
@ -641,13 +732,33 @@ public:
|
|||||||
static_cast<int>(glyph_height), fill);
|
static_cast<int>(glyph_height), fill);
|
||||||
DrawRectangleLinesEx(
|
DrawRectangleLinesEx(
|
||||||
Rectangle{left, top, slice_width, glyph_height}, 2.0f, edge);
|
Rectangle{left, top, slice_width, glyph_height}, 2.0f, edge);
|
||||||
|
while (g < group.size() && group[g]->instrument_slot == slot)
|
||||||
|
{
|
||||||
|
g++;
|
||||||
|
}
|
||||||
|
column++;
|
||||||
}
|
}
|
||||||
i = j;
|
i = j;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string score_text = "Score: " + std::to_string(score) + " Combo: " + std::to_string(combo);
|
std::string score_text = "Score: " + std::to_string(score);
|
||||||
DrawTextEx(font, score_text.c_str(), {20, 16}, 28, 1, WHITE);
|
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",
|
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});
|
{20, upper_bar_y - 28}, 18, 1, Color{200, 200, 200, 255});
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "engine/prefabs/includes.h"
|
#include "engine/prefabs/includes.h"
|
||||||
#include "entities/song.h"
|
#include "entities/song.h"
|
||||||
|
#include "entities/song_catalog.h"
|
||||||
#include "rapidjson/filereadstream.h"
|
#include "rapidjson/filereadstream.h"
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@ -58,8 +59,7 @@ public:
|
|||||||
{
|
{
|
||||||
auto font_manager = game->get_manager<FontManager>();
|
auto font_manager = game->get_manager<FontManager>();
|
||||||
font = font_manager->get_font("Roboto");
|
font = font_manager->get_font("Roboto");
|
||||||
std::vector<std::string> paths = {"assets/songs/json/mary.json"};
|
songs = build_song_list(get_song_catalog());
|
||||||
songs = build_song_list(paths);
|
|
||||||
selected_index = 0;
|
selected_index = 0;
|
||||||
scroll_offset = 0;
|
scroll_offset = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user