cakefoot/src/Cakefoot.cpp

1335 lines
50 KiB
C++

/*
* CAKEFOOT by @ohsqueezy https://ohsqueezy.itch.io
*/
#if defined(__ANDROID__) || defined(ANDROID)
#include <android/asset_manager_jni.h>
#endif
#include "Cakefoot.hpp"
Cakefoot::Cakefoot()
{
#ifdef __ANDROID__
SDL_SetHint(SDL_HINT_ORIENTATIONS, "Landscape");
#endif
/* Merge the level JSON */
configuration().merge("resource/levels.json");
/* Add default progress data */
configuration()["progress"] = {
{"current level", 1},
{"max level", 1},
{"current difficulty", 0},
{"max difficulty", 0},
{"current checkpoint", 0},
{"deaths", 0},
{"total time", 0.0f},
{"mirror", false}
};
/* Overwrite progress data with saved data if it is available */
if (fs::exists(progress_file_path))
{
configuration().merge(progress_file_path);
}
/* Set the level select and difficulty to what the player was last playing. */
level_select_index = configuration()("progress", "current level");
profile_index = configuration()("progress", "current difficulty");
/* Subscribe to command events */
delegate().subscribe(&Cakefoot::respond, this);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEMOTION);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEBUTTONDOWN);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEBUTTONUP);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEWHEEL);
/* Set up playing field, the plane that provides the background of the curve, character, and enemies */
sb::Plane playing_field_plane;
playing_field_plane.scale(glm::vec3{_configuration("display", "playing field aspect"), 1.0f, 1.0f});
playing_field = sb::Sprite(playing_field_plane);
/* Open the configuration and load the curve data per level */
load_curves();
/* loading GL context for 3D */
load_gl_context();
/* Load and fill VBO */
load_vbo();
/* Load cursors from the system library that will be freed automatically */
poke = std::shared_ptr<SDL_Cursor>(SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND), SDL_FreeCursor);
grab = std::shared_ptr<SDL_Cursor>(SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEALL), SDL_FreeCursor);
/* Set the character to use the profile stored to progress */
character.profile(configuration()("character", "profile", profile_index, "name"));
/* Try to load fonts. Default to the game object's font if the font doesn't load. */
for (auto [name, font_config] : configuration()("font").items())
{
fonts[name] = {TTF_OpenFont(font_config["path"].get<std::string>().c_str(), font_config["size"]), TTF_CloseFont};
std::ostringstream message;
if (fonts[name].get() == nullptr)
{
message << "Could not load " << font_config["path"] << ". Using framework's font instead.";
sb::Log::log(message, sb::Log::ERROR);
/* Default to framework's font */
fonts[name] = font();
}
else
{
message << "Loaded " << font_config["path"];
sb::Log::log(message);
}
}
/* Set up checkpoint on and off sprites */
checkpoint_on = sb::Sprite {"resource/checkpoint/on.png", glm::vec2(12.0f / 486.0f)};
checkpoint_off = sb::Sprite {"resource/checkpoint/off.png", glm::vec2(12.0f / 486.0f)};
/* Set hitbox */
character.box_size(configuration()("character", "hitbox").get<float>());
/* Load coin graphics */
coin.load();
/* Load SFX and BGM */
load_audio();
/* Load title screen and character graphics */
character.load();
load_level(0);
/* Switch volume on */
button.at("volume").press();
}
void Cakefoot::load_audio()
{
audio = {};
for (const auto& [name, path] : configuration()("audio", "files").items())
{
audio[name] = sb::audio::Chunk(path.get<fs::path>());
}
/* Set number of loops for looping audio */
audio.at("restart").loop(5);
audio.at("main").loop();
audio.at("menu").loop();
audio.at("walk").loop();
audio.at("reverse").loop();
/* Set volumes */
for (const auto& [name, volume] : configuration()("audio", "volume").items())
{
audio.at(name).volume(volume);
}
/* Reserve two channels for walk sound effects so the channel volume manipulation won't affect other sounds. */
int result = Mix_ReserveChannels(2);
if (result != 2)
{
sb::Log::sdl_error("Unable to reserve audio channels in SDL mixer", sb::Log::WARN);
}
}
void Cakefoot::load_curves()
{
/* Reset curve list and count of vertex buffer bytes needed for the curves */
curve_byte_count = 0;
curves.clear();
/* Open the levels section of the configuration and iterate through the list of levels. */
nlohmann::json levels = configuration()["levels"];
for (std::size_t ii = 0; ii < levels.size(); ii++)
{
/* Get the current level curve points, which is a list of 2D vertices in the old format of 864x486. The vertices are control points for
* the bezier. */
nlohmann::json control = levels[ii]["curve"];
glm::vec2 orig = {864.0f, 486.0f}, point;
float rat = orig.x / orig.y;
/* Translate each control point into the -aspect - aspect format. */
for (std::size_t jj = 0; jj < control.size(); jj++)
{
point = control[jj];
control[jj] = {(point.x / orig.x) * (2.0f * rat) - rat, (1.0f - point.y / orig.y) * 2.0f - 1.0f};
}
/* For each group of four control points, create a bezier, and add each of its vertices to a vector containing all the non-wrapped vertices, which
* is the full curve for the current level before being wrapped. */
std::vector<glm::vec3> unwrapped;
for (std::size_t jj = 0; jj < control.size() - 2; jj += 3)
{
std::vector<glm::vec3> segment;
for (const glm::vec2& vertex : sb::bezier({control[jj], control[jj + 1], control[jj + 2], control[jj + 3]},
_configuration("curve", "bezier resolution").get<int>()))
{
segment.push_back({vertex, 0.0f});
}
unwrapped.insert(unwrapped.end(), segment.begin(), segment.end());
}
/* Pass the vertices to a curve object, which will store the originals, then wrap them. */
Curve curve {rat};
curve.add(unwrapped);
curves.push_back(curve);
curve_byte_count += curve.size();
}
}
void Cakefoot::load_gl_context()
{
super::load_gl_context();
/* Generate a vertex array object ID, bind it as current (requirement of OpenGL) */
vao.generate();
sb::Log::gl_errors("after generating VAO");
vao.bind();
sb::Log::gl_errors("after binding VAO");
/* Load shader program */
GLuint vertex_shader = load_shader("src/shaders/shader.vert", GL_VERTEX_SHADER);
GLuint fragment_shader = load_shader("src/shaders/shader.frag", GL_FRAGMENT_SHADER);
shader_program = glCreateProgram();
glAttachShader(shader_program, vertex_shader);
sb::Log::gl_errors("after loading vertex shader");
glAttachShader(shader_program, fragment_shader);
sb::Log::gl_errors("after loading fragment shader");
/* link shaders */
link_shader(shader_program);
glUseProgram(shader_program);
sb::Log::gl_errors("after linking");
/* store uniform locations after linking */
uniform["mvp"] = glGetUniformLocation(shader_program, "mvp");
uniform["time"] = glGetUniformLocation(shader_program, "time");
uniform["uv transformation"] = glGetUniformLocation(shader_program, "uv_transformation");
uniform["coordinate bound"] = glGetUniformLocation(shader_program, "coordinate_bound");
uniform["model texture"] = glGetUniformLocation(shader_program, "model_texture");
uniform["texture enabled"] = glGetUniformLocation(shader_program, "texture_enabled");
uniform["color addition"] = glGetUniformLocation(shader_program, "color_addition");
/* Set the active texture and uniform once at context load time because only one texture is used per draw */
glActiveTexture(GL_TEXTURE0);
glUniform1i(uniform["model texture"], 0);
sb::Log::gl_errors("after uniform locations");
/* Initialize color addition to zero. */
glUniform4f(uniform["color addition"], 0.0f, 0.0f, 0.0f, 0.0f);
/* Enable alpha rendering, disable depth test, set clear color */
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glm::vec4 clear_color = _configuration("display", "clear color").get<glm::vec4>();
glClearColor(clear_color[0], clear_color[1], clear_color[2], clear_color[3]);
sb::Log::gl_errors("after GL initialization");
}
void Cakefoot::set_up_buttons()
{
/* Set up text buttons */
for (const std::string& name : {"start", "resume", "reset", "level increment", "level decrement", "profile increment", "profile decrement"})
{
sb::Text text {fonts.at("text")};
float scale;
glm::vec2 dimensions;
if (name == "start" || name == "resume" || name == "reset")
{
dimensions = {configuration()("button", "text dimensions")};
scale = configuration()("button", "text scale");
}
else
{
dimensions = {configuration()("button", "level decrement dimensions")};
if (configuration()("button", "level select scale").is_array())
{
scale = configuration()("button", "level select scale")[1];
}
else
{
scale = configuration()("button", "level select scale");
}
}
text.foreground(configuration()("button", "text foreground").get<glm::vec4>());
text.background(configuration()("button", "text background").get<glm::vec4>());
text.content(configuration()("button", name + " text"));
text.dimensions(dimensions);
button.at(name) = sb::Pad<>{text, configuration()("button", name + " translation"), scale, dimensions.y / dimensions.x};
}
button.at("start").on_state_change([&](bool state){
load_level(level_select_index);
});
button.at("resume").on_state_change([&](bool state){
unpaused_timer.on();
run_timer.on();
/* Transition between menu theme and main theme */
if (audio.at("menu").playing())
{
audio.at("menu").pause();
}
if (audio.at("main").paused())
{
audio.at("main").resume();
}
else if (audio.at("main").fading() || !audio.at("main").playing())
{
audio.at("main").play();
}
});
button.at("reset").on_state_change([&](bool state){
audio.at("main").stop();
sb::Delegate::post(reset_command_name, false);
});
/* Set up pause button */
sb::Texture pause_texture {configuration()("button", "pause texture")};
pause_texture.load();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
sb::Plane pause_plane;
pause_plane.texture(pause_texture);
button.at("pause") = sb::Pad<>{pause_plane, configuration()("button", "pause translation"), configuration()("button", "pause scale"), 1.0f};
button.at("pause").on_state_change([&](bool state){
unpaused_timer.off();
run_timer.off();
std::cout << std::boolalpha << "menu playing: " << audio.at("menu").playing() << ", menu fading: " << audio.at("menu").fading() << std::endl;
/* Transition between main theme and menu theme */
if (audio.at("main").playing())
{
audio.at("main").pause();
}
if (audio.at("menu").paused())
{
audio.at("menu").resume();
}
else if (audio.at("menu").fading() || !audio.at("menu").playing())
{
audio.at("menu").play();
}
std::cout << std::boolalpha << "menu playing: " << audio.at("menu").playing() << ", menu fading: " << audio.at("menu").fading() << std::endl;
});
/* Set up volume button */
bool original_state = button.at("volume").pressed();
sb::Texture volume_off_texture {configuration()("button", "volume off texture")};
volume_off_texture.load();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
sb::Texture volume_on_texture {configuration()("button", "volume on texture")};
volume_on_texture.load();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
sb::Plane volume_plane;
volume_plane.texture(volume_off_texture);
volume_plane.texture(volume_on_texture);
button.at("volume") = sb::Pad<>{volume_plane, configuration()("button", "volume translation"), configuration()("button", "volume scale"), 1.0f};
button.at("volume").state(original_state);
button.at("volume").on_state_change([&](bool state){
/* BGM is paused to get around a bug in looping that causes looped audio to stop when looping on mute. */
if (state)
{
Mix_Volume(-1, 128);
// for (auto& [name, chunk] : audio)
// {
// chunk.channel_volume(MIX_MAX_VOLUME);
// }
}
else
{
for (auto& [name, chunk] : audio)
{
if (chunk.fading())
{
chunk.stop();
chunk.play();
}
// chunk.channel_volume(0);
}
Mix_Volume(-1, 0);
}
});
/* Set up level select spinner */
for (const std::string& name : {"profile", "level select"})
{
glm::vec2 dimensions {configuration()("button", name + " dimensions")};
label.at(name).foreground(configuration()("button", "text foreground").get<glm::vec4>());
label.at(name).background(configuration()("button", "text background").get<glm::vec4>());
label.at(name).untransform();
std::ostringstream message;
message << configuration()("button", name + " text").get<std::string>();
if (name == "profile")
{
message << configuration()("character", "profile", profile_index, "name").get<std::string>();
}
else if (name == "level select")
{
message << level_select_index;
}
label.at(name).content(message.str());
label.at(name).translate(configuration()("button", name + " translation"));
label.at(name).scale(configuration()("button", name + " scale"));
label.at(name).dimensions(dimensions);
}
button.at("profile decrement").on_state_change([&](bool state){
profile_index--;
if (profile_index < 0)
{
profile_index = configuration()("progress", "max difficulty");
}
configuration()["progress"]["current difficulty"] = profile_index;
write_progress();
set_up_buttons();
character.profile(configuration()("character", "profile", profile_index, "name"));
});
button.at("profile increment").on_state_change([&](bool state){
profile_index++;
if (profile_index > configuration()("progress", "max difficulty"))
{
profile_index = 0;
}
if (profile_index == configuration()("progress", "max difficulty"))
{
/* If the max difficulty was selected, clamp the level to the max level. */
if (level_select_index > configuration()("progress", "max level"))
{
level_select_index = configuration()("progress", "max level");
}
}
configuration()["progress"]["current difficulty"] = profile_index;
write_progress();
set_up_buttons();
character.profile(configuration()("character", "profile", profile_index, "name"));
});
button.at("level decrement").on_state_change([&](bool state){
/* If the level is decreased below 1, wrap to the last level if the current difficulty is complete, otherwise wrap to the max level
* unlocked. */
if (--level_select_index < 1)
{
if (profile_index < configuration()("progress", "max difficulty"))
{
level_select_index = configuration()("levels").size() - 2;
}
else
{
level_select_index = configuration()("progress", "max level");
}
}
configuration()["progress"]["current level"] = level_select_index;
write_progress();
set_up_buttons();
});
button.at("level increment").on_state_change([&](bool state){
/* If the level is increased past the total number of levels or past the max level unlocked on the current difficulty,
* wrap the spinner back to 1. */
if (++level_select_index >= static_cast<int>(configuration()("levels").size() - 1) || (
profile_index == configuration()("progress", "max difficulty") && level_select_index > configuration()("progress", "max level")))
{
level_select_index = 1;
}
configuration()["progress"]["current level"] = level_select_index;
write_progress();
set_up_buttons();
});
}
void Cakefoot::set_up_hud()
{
/* Shrink the FPS indicator, and give it an opaque black background. */
float modifier = configuration()("display", "fps indicator scale");
glm::vec3 scale = {modifier, modifier * window_box().aspect(), 1.0f};
label.at("fps").foreground({255.0f, 255.0f, 255.0f, 255.0f});
label.at("fps").background({0.0f, 0.0f, 0.0f, 255.0f});
label.at("fps").untransform();
label.at("fps").translate({1.0f - scale.x, 1.0f - scale.y, 0.0f});
label.at("fps").scale(scale);
/* Blow up the time if it's the last level. */
glm::vec3 clock_scale, clock_translation;
if (static_cast<std::size_t>(level_index) == _configuration("levels").size() - 1)
{
clock_scale = configuration()("display", "clock hud large scale");
clock_translation = configuration()("display", "clock hud large translation");
}
/* Otherwise, set it to standard HUD size. */
else
{
clock_scale = configuration()("display", "clock hud scale");
clock_translation = configuration()("display", "clock hud translation");
}
/* Style the clock */
label.at("clock").foreground(configuration()("display", "clock hud foreground").get<glm::vec4>());
label.at("clock").background(configuration()("display", "clock hud background").get<glm::vec4>());
label.at("clock").untransform();
label.at("clock").translate(clock_translation);
label.at("clock").scale(clock_scale);
/* Style the level indicator */
label.at("level").foreground(configuration()("display", "level hud foreground").get<glm::vec4>());
label.at("level").background(configuration()("display", "level hud background").get<glm::vec4>());
label.at("level").untransform();
label.at("level").translate(configuration()("display", "level hud translation"));
label.at("level").scale(configuration()("display", "level hud scale"));
}
void Cakefoot::load_vbo()
{
/* Generate ID for the vertex buffer object that will hold all vertex data. Using one buffer for all attributes, data
* will be copied in one after the other. */
vbo.generate();
vbo.bind();
sb::Log::gl_errors("after generating and binding VBO");
/*!
* Fill VBO with attribute data:
*
* Postion, UV, and color vertices for a single sb::Plane, and curve.
*/
vbo.allocate(sb::Plane().size() + playing_field.attributes("color")->size() + curve_byte_count, GL_STATIC_DRAW);
vbo.add(*sb::Plane::position);
vbo.add(*sb::Plane::uv);
vbo.add(*sb::Plane::color);
for (Curve& curve : curves)
{
for (sb::Attributes& attr : curve.position)
{
vbo.add(attr);
}
vbo.add(curve.color);
}
sb::Log::gl_errors("after filling VBO");
/* Bind UV attributes once at load time because they will not be changing */
sb::Plane::uv->bind("vertex_uv", shader_program);
}
Curve& Cakefoot::curve()
{
return curves[curve_index % curves.size()];
}
void Cakefoot::load_level(int index)
{
/* Wrap the index if it is out of range. */
index = glm::mod(index, configuration()("levels").size());
/* Play menu theme on title screen and end screen. Play main theme on any other level. Cross fade between the two. */
// float fade = configuration()("audio", "fade");
if (index == 0 || static_cast<std::size_t>(index) == configuration()("levels").size() - 1)
{
/* If menu theme is already playing, let it continue to play. */
// audio.at("menu").stop();
if (!audio.at("menu").playing() || audio.at("menu").paused() || audio.at("menu").fading())
{
// audio.at("menu").play(fade);
audio.at("menu").play();
}
/* Cross fade main theme into menu theme */
// if (audio.at("main").playing())
// {
// audio.at("main").stop(fade);
audio.at("main").stop();
// }
}
else
{
// audio.at("main").stop();
if (audio.at("main").paused() || !audio.at("main").playing())
{
// audio.at("main").play(fade);
audio.at("main").play();
}
/* If the menu theme is playing, cross fade to the main theme. */
// if (audio.at("menu").playing())
// {
// audio.at("menu").stop(fade);
audio.at("menu").stop();
// }
}
/* Update indices and reset character. */
level_index = index;
curve_index = index;
character.beginning(curve());
/* The wrap space of the field is necessary for flame enemy objects */
sb::Box field {-curve().aspect, -1.0f, 2.0f * curve().aspect, 2.0f};
/* If the level is the end screen, reset the player's current level to the beginning. Otherwise, if the level is not the title screen, save it as
* the current level in the player's progress. Also save the newly assigned current level in the level select index. */
if (static_cast<std::size_t>(index) == configuration()("levels").size() - 1)
{
configuration()["progress"]["current level"] = 1;
/* Increase difficulty if there is a higher difficulty than the current one. Then reset the max level because it will now refer to max
* level of the next difficulty. */
if (profile_index < static_cast<int>(configuration()("character", "profile").size()) - 1)
{
profile_index++;
configuration()["progress"]["max difficulty"] = profile_index;
configuration()["progress"]["max level"] = 1;
character.profile(configuration()("character", "profile", profile_index, "name"));
}
write_progress();
level_select_index = 1;
}
else if (index > 0)
{
/* If the current difficulty is the max difficulty, increase the value of the max level reached in the progress if necessary */
if (configuration()("progress", "max difficulty") == profile_index && configuration()("progress", "max level").get<int>() < index)
{
configuration()["progress"]["max level"] = index;
}
configuration()["progress"]["current level"] = index;
write_progress();
level_select_index = index;
}
/* Reset enemies list to empty. Open configuration for the current level. Repopulate list of enemies one by one using the list of enemies in the
* configuration. For each enemy, add a challenge coin if the config specifies the coin parameters.
*
* Values read from the config are in some cases converted from old 25fps hard-coded per-frame values to per-second values, and hard-coded
* 864px by 486px pixel space to relative NDC space.
*/
this->enemies.clear();
if (configuration()("levels", index).contains("enemies"))
{
nlohmann::json enemies = configuration()("levels", index, "enemies");
for (std::size_t ii = 0; ii < enemies.size(); ii++)
{
nlohmann::json enemy = enemies[ii];
std::string type = enemy[0];
if (type == "slicer")
{
std::shared_ptr<Slicer> slicer = std::make_shared<Slicer>(
curve(), enemy[1].get<float>(), 2.0f * 25.0f * enemy[2].get<float>() / 486.0f, 2.0f * enemy[3].get<float>() / 486.0f);
/* Add coin to slicer */
if (enemy.size() > 4)
{
slicer->coin(coin, enemy[4].get<float>(), enemy[5].get<float>());
}
this->enemies.push_back(slicer);
}
else if (type == "fish")
{
std::shared_ptr<Fish> fish = std::make_shared<Fish>(
curve(), enemy[1].get<float>(), 25.0f * enemy[2].get<float>(), 2.0f * enemy[3].get<float>() / 486.0f,
enemy[4].get<float>());
/* Add coin to fish */
if (enemy.size() > 6)
{
fish->coin(coin, enemy[5].get<float>(), enemy[6].get<float>());
}
else if (enemy.size() > 5)
{
fish->coin(coin, enemy[5].get<float>());
}
this->enemies.push_back(fish);
}
else if (type == "projector")
{
std::shared_ptr<Projector> projector = std::make_shared<Projector>(
character,
(glm::vec3{2.0f * 1.7777f, 2.0f, 1.0f} * enemy[1].get<glm::fvec3>() / glm::vec3{864.0f, 486.0f, 1.0f} -
glm::vec3(1.77777f, 1.0f, 0.0f)) * glm::vec3(1.0f, -1.0f, 0.0f),
2.0f * 25.0f * enemy[2].get<float>() / 486.0, enemy[3].get<float>(), enemy[4].get<float>());
/* Add coin to projector */
if (enemy.size() > 5)
{
projector->coin(coin, enemy[5].get<float>());
}
this->enemies.push_back(projector);
}
else if (type == "flame")
{
std::shared_ptr<Flame> flame = std::make_shared<Flame>(
field, enemy[1].get<glm::fvec3>(), enemy[2].get<float>(), enemy[3].get<float>(), enemy[4].get<float>());
/* Add coin to flame */
if (enemy.size() > 5)
{
flame->coin(coin, enemy[5], enemy[6]);
}
this->enemies.push_back(flame);
}
else if (type == "grid")
{
/* Add a grid of flame objects */
float y = field.top();
glm::vec2 margin {0.59259f, 0.5f};
bool shift = false;
int count = 0;
while (y > field.bottom())
{
float x = field.left() + shift * margin.x / 2.0f;
while (x < field.right())
{
std::shared_ptr<Flame> flame = std::make_shared<Flame>(field, glm::vec3{x, y, 0.0f}, 0.41152263f, glm::quarter_pi<float>());
/* Add a challenge coin */
if (++count == 15)
{
flame->coin(coin, margin.x / 2.0f, 1.57f);
}
this->enemies.push_back(flame);
x += margin.x;
}
shift = !shift;
y -= margin.y;
}
}
else if (type == "wave")
{
/* Add a wave of flame objects */
float y = 0.0f;
float speed = enemy[4].get<float>();
float amplitude = enemy[1].get<float>();
float period = enemy[2].get<float>();
float step = enemy[3].get<float>();
float shift = enemy[5].get<float>();
float mirror = -1.0f;
if (enemy.size() > 7)
{
mirror = enemy[7].get<float>();
}
glm::vec2 range {field.left(), field.right()};
if (enemy.size() > 6)
{
range = enemy[6].get<glm::vec2>();
}
float x = range.x;
for (std::size_t count = 0; x < range.y; ++count)
{
y = amplitude * glm::sin(period * x) + shift;
std::shared_ptr<Flame> flame = std::make_shared<Flame>(field, glm::vec3{x, y, 0.0f}, speed, 3.0f * glm::half_pi<float>(), mirror);
if (enemy.size() > 8 && enemy[8].get<std::size_t>() == count)
{
flame->coin(coin, enemy[9].get<float>(), enemy[10].get<float>());
}
this->enemies.push_back(flame);
x += step;
}
}
}
}
/* If it's the first or last level, stop the run timer. Otherwise, turn it on. */
if (index == 0 || static_cast<std::size_t>(index) == _configuration("levels").size() - 1)
{
run_timer.off();
}
else
{
run_timer.on();
/* If it's the first level, reset the clock. */
if (index == 1)
{
run_timer.reset();
}
}
/* Refresh HUD elements */
set_up_hud();
/* Refresh the buttons so the level select will reflect the change in level */
set_up_buttons();
/* Set the color background according to current world */
nlohmann::json world = configuration()("world");
for (std::size_t ii = 0; ii < world.size(); ii++)
{
if (ii == world.size() - 1 || world[ii + 1].at("start").get<int>() > index)
{
world_color = world[ii].at("color").get<glm::fvec4>();
break;
}
}
}
void Cakefoot::write_progress()
{
if (!fs::exists("storage"))
{
try
{
fs::create_directory("storage");
}
catch (fs::filesystem_error error)
{
std::ostringstream message;
message << "Could not create storage directory. Progress will not be saved. " << error.what();
sb::Log::log(message, sb::Log::ERROR);
}
}
std::ofstream progress_file {progress_file_path};
nlohmann::json progress {
{"progress", configuration()("progress")}
};
if (progress_file << std::setw(4) << progress << std::endl)
{
std::ostringstream message;
message << "Successfully saved progress to " << progress_file_path;
sb::Log::log(message);
}
else
{
std::ostringstream message;
message << "Could not save progress to " << progress_file_path;
sb::Log::log(message, sb::Log::ERROR);
}
progress_file.close();
#if defined(EMSCRIPTEN)
EM_ASM(
FS.syncfs(false, function(error) {
if (error !== null)
{
console.log("Error syncing storage using Filesystem API", error);
}
});
collectData();
);
#endif
}
void Cakefoot::respond(SDL_Event& event)
{
bool left_mouse_pressed = SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON_LMASK;
bool shift_pressed = SDL_GetModState() & KMOD_SHIFT;
/* Custom keys for the title screen */
if (level_index == 0)
{
if (!shift_pressed && sb::Delegate::compare(event, "left"))
{
button.at("level decrement").press();
}
else if (!shift_pressed && sb::Delegate::compare(event, "right"))
{
button.at("level increment").press();
}
else if (sb::Delegate::compare(event, "any"))
{
button.at("start").press();
}
}
/* Perspective and view modifications */
if (event.type == SDL_MOUSEWHEEL && shift_pressed)
{
/* Edit zoom level with mouse wheel, which will modify the FOV */
zoom -= event.wheel.preciseY * glm::radians(2.0f);
if (zoom < glm::radians(-30.0f))
{
zoom = glm::radians(-30.0f);
}
else if (zoom > glm::radians(30.0f))
{
zoom = glm::radians(30.0f);
}
}
else if (event.type == SDL_MOUSEBUTTONUP || sb::Delegate::compare_cancel(event, "any"))
{
/* End character acceleration */
if (sb::Delegate::compare_cancel(event, "any") || (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT))
{
character.accelerating = false;
}
}
else if (event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEBUTTONDOWN || sb::Delegate::compare(event, "any"))
{
/* Get mouse coordinates in pixel resolution and NDC */
glm::vec2 mouse_pixel = event.type == SDL_MOUSEBUTTONDOWN ? glm::vec2{event.button.x, event.button.y} :
glm::vec2{event.motion.x, event.motion.y};
glm::vec2 mouse_ndc {
float(mouse_pixel.x) / window_box().width() * 2.0f - 1.0f, (1.0f - float(mouse_pixel.y) / window_box().height()) * 2.0f - 1.0f
};
/* Track whether cursor should display */
bool hovering = false;
/* Track whether a button has been pressed with this event */
bool button_pressed = false;
/* Collide with start button and spinners only on title screen */
if (level_index == 0)
{
for (const std::string& name : {"start", "level increment", "level decrement", "profile increment", "profile decrement"})
{
if (button.at(name).collide(mouse_ndc, view, projection))
{
hovering = true;
if (event.type == SDL_MOUSEBUTTONDOWN)
{
button.at(name).press();
/* Cancel hover on the start button because the button will be removed from the screen after the press. */
if (name == "start")
{
hovering = false;
}
}
}
}
}
/* Collide with pause button only during levels */
if (level_index > 0 && unpaused_timer && button.at("pause").collide(mouse_ndc, view, projection))
{
if (event.type == SDL_MOUSEBUTTONDOWN)
{
button.at("pause").press();
button_pressed = true;
}
else
{
hovering = true;
}
}
/* Collide with volume button */
else if (button.at("volume").collide(mouse_ndc, view, projection))
{
if (event.type == SDL_MOUSEBUTTONDOWN)
{
button.at("volume").press();
button_pressed = true;
}
else
{
hovering = true;
}
}
/* Check pause menu buttons */
else if (level_index > 0 && !unpaused_timer)
{
for (const std::string& button_name : {"resume", "reset"})
{
if (button.at(button_name).collide(mouse_ndc, view, projection))
{
if (event.type == SDL_MOUSEBUTTONDOWN)
{
button.at(button_name).press();
button_pressed = true;
}
else
{
hovering = true;
}
}
}
}
/* Rotate scene */
if (event.type == SDL_MOUSEMOTION && left_mouse_pressed && shift_pressed)
{
rotation += glm::vec2{event.motion.xrel, event.motion.yrel} * glm::half_pi<float>() * 0.005f;
}
/* Start character acceleration */
bool acceleration_pressed = ((event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_LEFT) || sb::Delegate::compare(event, "any"));
if (!shift_pressed && !button_pressed && acceleration_pressed)
{
character.accelerating = true;
}
/* Set the cursor image appropriately */
if (hovering && SDL_GetCursor() == SDL_GetDefaultCursor())
{
SDL_SetCursor(poke.get());
}
else if (!hovering && SDL_GetCursor() == poke.get())
{
SDL_SetCursor(SDL_GetDefaultCursor());
}
}
else if (sb::Delegate::compare(event, "fps"))
{
configuration()["display"]["fps"] = !configuration()["display"]["fps"];
}
else if (sb::Delegate::compare(event, "skip forward"))
{
load_level(level_index + 1);
}
else if (sb::Delegate::compare(event, "skip backward"))
{
load_level(level_index - 1);
}
else if (sb::Delegate::compare(event, "reset"))
{
zoom = 0.0f;
rotation = {0.0f, 0.0f};
load_level(0);
unpaused_timer.on();
run_timer.reset();
}
else if (sb::Delegate::compare(event, "reconfig"))
{
load_curves();
load_vbo();
load_level(level_index);
character.box_size(configuration()("character", "hitbox").get<float>());
set_up_buttons();
set_up_hud();
load_audio();
}
else if (sb::Delegate::compare(event, "window resize"))
{
set_up_buttons();
set_up_hud();
}
/* Taken from mallinfo man page, log a profile of the memory when the command is sent. */
else if (sb::Delegate::compare(event, "memory"))
{
/* Struct with quantified memory allocation information. */
#if !defined(EMSCRIPTEN)
struct mallinfo2 malloc_info = mallinfo2();
#else
struct mallinfo malloc_info = mallinfo();
#endif
/* Create a map from the struct's member variables. */
std::map<std::string, int> malloc_map = {
{"Total non-mmapped bytes (arena):", malloc_info.arena},
{"# of free chunks (ordblks):", malloc_info.ordblks},
{"# of free fastbin blocks (smblks):", malloc_info.smblks},
{"# of mapped regions (hblks):", malloc_info.hblks},
{"Bytes in mapped regions (hblkhd):", malloc_info.hblkhd},
{"Max. total allocated space (usmblks):", malloc_info.usmblks},
{"Free bytes held in fastbins (fsmblks):", malloc_info.fsmblks},
{"Total allocated space (uordblks):", malloc_info.uordblks},
{"Total free space (fordblks):", malloc_info.fordblks},
{"Topmost releasable block (keepcost):", malloc_info.keepcost},
};
/* Loop through the map, and print each value. */
std::ostringstream message;
int first_column = 40, second_column = 12, count = 0;
message << std::endl;
for (std::pair<std::string, int> malloc_info_entry : malloc_map)
{
message << std::setw(first_column) << malloc_info_entry.first << std::setw(second_column) << std::setprecision(2)
<< std::fixed << malloc_info_entry.second / 1000.0 << " KB";
if ((++count % 3) == 0)
{
message << std::endl;
}
}
sb::Log::log(message);
}
/* Print the coordinates of the cake sprite in all coordinate spaces */
else if (sb::Delegate::compare(event, "coords"))
{
std::ostringstream message;
glm::vec2 translation = sb::wrap_point(character.position, {-curve().aspect, -1.0f, 0.0f}, {curve().aspect, 1.0f, 1.0f});
message << std::fixed << std::setprecision(2) << "Character coords: unwrapped " << character.position << ", wrapped " << translation
<< ", clip " << sb::world_to_clip(translation, projection * view) << ", ndc " << sb::world_to_ndc(translation, projection * view)
<< ", window " << sb::world_to_viewport(translation, window_box().size(), projection * view);
sb::Log::log(message);
}
}
void Cakefoot::run()
{
/* Start timers precisely when the game update loop starts */
on_timer.on();
unpaused_timer.on();
/* Enable auto refresh */
#if defined(__LINUX__)
configuration().enable_auto_refresh("resource/levels.json");
#endif
/* Start the update loop */
Game::run();
}
void Cakefoot::update(float timestamp)
{
sb::Log::gl_errors("at beginning of update");
/* Update time in seconds the game has been running for, pass to the shader. */
on_timer.update(timestamp);
glUniform1f(uniform["time"], on_timer.elapsed());
/* Update other timers */
run_timer.update(timestamp);
configuration()["progress"]["total time"].get_ref<nlohmann::json::number_float_t&>() += run_timer.frame();
unpaused_timer.update(timestamp);
/* Update character, along the curve, using the timer to determine movement since last frame, and update enemies. Check for collison
* as enemies are updated. */
character.update(curve(), unpaused_timer, !button.at("volume").pressed());
if (character.at_end(curve()))
{
audio.at("teleport").play();
load_level(level_index + 1);
}
else
{
/* Update checkpoint */
if (profile_index == 0 && configuration()("levels", level_index).contains("checkpoints"))
{
for (nlohmann::json checkpoint : configuration()("levels", level_index, "checkpoints"))
{
if (character.relative(curve()) >= checkpoint["position"].get<float>() && character.checkpoint() < checkpoint["position"].get<float>())
{
audio.at("checkpoint").play();
character.checkpoint(checkpoint["position"].get<float>());
/* Collect any previously taken coins */
for (auto& enemy : enemies)
{
if (enemy->coin_taken())
{
enemy->collect_coin();
}
}
}
}
}
/* Collide with enemies and challenge coins */
bool enemy_collision = false;
glm::vec3 clip_upper {-curve().aspect, -1.0f, -1.0f}, clip_lower {curve().aspect, 1.0f, 1.0f};
for (auto& enemy : enemies)
{
enemy->update(unpaused_timer);
if (enemy->collide(character.box(), character.sprite(), clip_upper, clip_lower))
{
enemy_collision = true;
}
else if (enemy->collide_coin(character.box(), clip_upper, clip_lower))
{
audio.at("take").play();
enemy->take_coin();
}
}
/* Reset level */
if (!character.resting() && enemy_collision)
{
audio.at("restart").play();
character.spawn(curve());
for (auto& enemy : enemies)
{
enemy->reset();
}
/* Record a death */
configuration()["progress"]["deaths"].get_ref<nlohmann::json::number_integer_t&>()++;
}
}
/* Transformation for looking at the center of the field of play from the camera position. */
view = glm::lookAt(camera_position, {0.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
glm::mat4 rotation_matrix = glm::rotate(glm::mat4(1), rotation.x, {0.0f, 1.0f, 0.0f}) * glm::rotate(glm::mat4(1), rotation.y, {1.0f, 0.0f, 0.0f});
/* Transformation from camera space to clip space. */
float fov;
if (window_box().aspect() >= curve().aspect)
{
fov = 2.0f * glm::atan(1.0f / camera_position.z) + zoom;
}
else
{
fov = 2.0f * glm::atan(((1.0f / (window_box().width() * (9.0f / 16.0f))) * window_box().height()) / camera_position.z) + zoom;
}
projection = glm::perspective(fov, window_box().aspect(), 0.1f, 100.0f);
/* Plane position vertices will be used for everything before the curve */
sb::Plane::position->bind("vertex_position", shader_program);
/* Clear screen to configured color */
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/* Disable texture, set background color for the current world using the color addition uniform, and draw playing field (background) */
glUniform1i(uniform["texture enabled"], false);
glUniform4fv(uniform["color addition"], 1, &world_color[0]);
playing_field.attributes("color")->bind("vertex_color", shader_program);
playing_field.attributes("color")->enable();
playing_field.draw(uniform["mvp"], view * rotation_matrix, projection, uniform["texture enabled"]);
playing_field.attributes("color")->disable();
/* Reset color addition, and draw curve. */
glm::mat4 vp = projection * view * rotation_matrix;
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &vp[0][0]);
curve().color.bind("vertex_color", shader_program);
curve().color.enable();
for (sb::Attributes& position : curve().position)
{
position.bind("vertex_position", shader_program);
position.enable();
glDrawArrays(GL_LINE_STRIP, 0, position.count());
position.disable();
}
curve().color.disable();
/* Draw checkpoints */
sb::Plane::position->bind("vertex_position", shader_program);
sb::Plane::color->bind("vertex_color", shader_program);
if (profile_index == 0 && configuration()("levels", level_index).contains("checkpoints"))
{
for (nlohmann::json checkpoint : configuration()("levels", level_index, "checkpoints"))
{
sb::Sprite* sprite;
if (checkpoint["position"].get<float>() > character.checkpoint())
{
sprite = &checkpoint_off;
}
else
{
sprite = &checkpoint_on;
}
glm::vec3 position = curve().relative(checkpoint["position"].get<float>());
glm::vec2 delta = sb::angle_to_vector(checkpoint["angle"].get<float>(), configuration()("display", "checkpoint distance"));
position += glm::vec3{delta.x, delta.y, 0.0f};
sprite->translate(curve().wrap(position));
sprite->draw(uniform.at("mvp"), view * rotation_matrix, projection, uniform.at("texture enabled"));
}
}
/* Draw enemies */
for (auto& enemy : enemies)
{
enemy->draw(uniform["mvp"], view * rotation_matrix, projection, uniform["texture enabled"]);
}
/* Draw cake */
character.draw(curve(), uniform["mvp"], view * rotation_matrix, projection, uniform["texture enabled"]);
/* Draw buttons. Don't include rotation matrix in view, so buttons will remain flat in the z-dimension. */
button.at("volume").draw(uniform["mvp"], view, projection, uniform["texture enabled"]);
if (level_index == 0)
{
button.at("start").draw(uniform["mvp"], view, projection, uniform["texture enabled"]);
/* Draw spinner buttons */
for (const std::string& name : {"level decrement", "level increment", "profile decrement", "profile increment"})
{
button.at(name).draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
}
/* Draw spinner labels */
for (const std::string& name : {"level select", "profile"})
{
label.at(name).texture(0).bind();
glm::mat4 label_transformation = projection * view * label.at(name).transformation();
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_transformation[0][0]);
label.at(name).enable();
glDrawArrays(GL_TRIANGLES, 0, label.at(name).attributes("position")->count());
}
}
else
{
if (unpaused_timer)
{
button.at("pause").draw(uniform["mvp"], view, projection, uniform["texture enabled"]);
}
else
{
for (std::string name : {"resume", "reset"})
{
button.at(name).draw(uniform["mvp"], view, projection, uniform["texture enabled"]);
}
}
}
/* Draw the clock */
int minutes = int(run_timer.elapsed()) / 60;
float seconds = run_timer.elapsed() - (minutes * 60);
std::stringstream clock;
clock << std::setw(2) << std::setfill('0') << minutes << ":" << std::setw(4) << std::setprecision(1) << std::fixed << seconds;
label.at("clock").content(clock.str());
sb::Plane::position->bind("vertex_position", shader_program);
glUniform1i(uniform["texture enabled"], true);
label.at("clock").texture(0).bind();
glm::mat4 clock_transformation = projection * view * label.at("clock").transformation();
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &clock_transformation[0][0]);
label.at("clock").enable();
glDrawArrays(GL_TRIANGLES, 0, label.at("clock").attributes("position")->count());
/* Draw the level indicator */
if (level_index > 0 && static_cast<std::size_t>(level_index) < _configuration("levels").size() - 1)
{
std::stringstream level_indicator;
level_indicator << std::setw(2) << std::setfill('0') << level_index << "/" << std::setw(2) << _configuration("levels").size() - 2;
label.at("level").content(level_indicator.str());
label.at("level").texture(0).bind();
glm::mat4 level_transformation = projection * view * label.at("level").transformation();
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &level_transformation[0][0]);
label.at("level").enable();
glDrawArrays(GL_TRIANGLES, 0, label.at("level").attributes("position")->count());
}
/* Update FPS indicator display to the current FPS count and draw. */
if (configuration()["display"]["fps"])
{
if (current_frames_per_second != previous_frames_per_second)
{
std::string padded = sb::pad(current_frames_per_second, 2);
label.at("fps").content(padded);
previous_frames_per_second = current_frames_per_second;
}
if (label.at("fps").texture(0).generated())
{
/* Draw FPS indicator */
sb::Plane::position->bind("vertex_position", shader_program);
glUniform1i(uniform["texture enabled"], true);
label.at("fps").texture(0).bind();
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label.at("fps").transformation()[0][0]);
label.at("fps").enable();
glDrawArrays(GL_TRIANGLES, 0, label.at("fps").attributes("position")->count());
}
}
/* Update display */
SDL_GL_SwapWindow(window());
sb::Log::gl_errors("at end of update");
}
int main()
{
Cakefoot game = Cakefoot();
game.run();
game.quit();
return 0;
}