3195 lines
130 KiB
C++
3195 lines
130 KiB
C++
/* _ _
|
|
* c/a`k-e'f`o^o~t-, | a single-button action game | @dankd0tgame
|
|
* / _< | wow a living cake the sweet | https://cakefoot.dank.game
|
|
* > `~_/ | taste of victory | https://open.shampoo.ooo/shampoo/cakefoot
|
|
*/
|
|
|
|
#if defined(__ANDROID__) || defined(ANDROID)
|
|
#include <android/asset_manager_jni.h>
|
|
#endif
|
|
|
|
#include "Cakefoot.hpp"
|
|
|
|
Cakefoot::Cakefoot(std::initializer_list<std::string> configuration_merge) : Game(configuration_merge)
|
|
{
|
|
#if defined(__ANDROID__)
|
|
SDL_SetHint(SDL_HINT_ORIENTATIONS, "Landscape");
|
|
#endif
|
|
|
|
/* Merge the level JSON */
|
|
configuration().merge(levels_file_path);
|
|
|
|
/* Load scores */
|
|
if (fs::exists(arcade_scores_file_path))
|
|
{
|
|
for (nlohmann::json score : Configuration::json_from_file(arcade_scores_file_path))
|
|
{
|
|
arcade_scores.add(ArcadeScores::Score(score.at("time"), score.at("distance"), score.at("name")));
|
|
}
|
|
}
|
|
|
|
/* Add default progress data */
|
|
configuration()["progress"] = {
|
|
{"current level", 1},
|
|
{"max level", 1},
|
|
{"current difficulty", 0},
|
|
{"max difficulty", 0},
|
|
{"current challenge", 1},
|
|
{"max challenge", 4},
|
|
{"current view", 0},
|
|
{"max view", 0},
|
|
{"deaths", 0},
|
|
{"total time", 0.0f},
|
|
{"quest level", 1},
|
|
{"quest checkpoint", 0.0f},
|
|
{"quest difficulty", 0},
|
|
{"quest time", 0.0f},
|
|
{"quest coin", false},
|
|
{"quest bank", 0},
|
|
{"quest best", 0.0},
|
|
{"arcade level", 1},
|
|
{"arcade checkpoint", 0.0f},
|
|
{"arcade difficulty", 0},
|
|
{"arcade max distance", 0},
|
|
{"arcade time", 0.0f},
|
|
{"arcade coin", false},
|
|
{"arcade bank", 0},
|
|
{"jackpot", 0}
|
|
};
|
|
|
|
/* Overwrite progress data with saved data if it is available */
|
|
if (fs::exists(progress_file_path))
|
|
{
|
|
/* Check progress data for existence of quest progress entries. If they are missing, it indicates an older progress file, so
|
|
* use the general progress values for the quest progress. */
|
|
nlohmann::json progress = Configuration::json_from_file(progress_file_path);
|
|
if (!progress.at("progress").contains("quest level"))
|
|
{
|
|
configuration()["progress"]["quest level"] = progress.at("progress").at("current level");
|
|
configuration()["progress"]["current challenge"] = 0;
|
|
}
|
|
if (!progress.at("progress").contains("quest checkpoint"))
|
|
{
|
|
configuration()["progress"]["quest difficulty"] = progress.at("progress").at("current difficulty");
|
|
}
|
|
if (!progress.at("progress").contains("quest time"))
|
|
{
|
|
configuration()["progress"]["quest time"] = progress.at("progress").at("total time");
|
|
}
|
|
|
|
/* Merge save data to overwrite defaults */
|
|
configuration().merge(progress_file_path);
|
|
}
|
|
|
|
/* Enforce arcade-only mode */
|
|
if (configuration()("display", "arcade only"))
|
|
{
|
|
configuration()["progress"]["current challenge"] = 4;
|
|
configuration()["progress"]["current difficulty"] = 0;
|
|
|
|
/* In arcade-only, there is no resume, so reset the current level regardless of what the saved state was. */
|
|
configuration()["progress"]["current level"] = 1;
|
|
}
|
|
|
|
/* Enforce max challenge to 4 */
|
|
configuration()["progress"]["max challenge"] = 4;
|
|
if (configuration()("progress", "current challenge") > 4)
|
|
{
|
|
configuration()["progress"]["current challenge"] = 4;
|
|
}
|
|
|
|
/* Set the spinner values to what the player was last playing, unless demo mode is active, in which case leave
|
|
* the values at the defaults. */
|
|
if (!configuration()("demo", "active"))
|
|
{
|
|
level_select_index = configuration()("progress", "current level");
|
|
profile_index = configuration()("progress", "current difficulty");
|
|
challenge_index = configuration()("progress", "current challenge");
|
|
view_index = configuration()("progress", "current view");
|
|
}
|
|
|
|
/* Initialize name entry */
|
|
name_entry = configuration()("display", "default initials");
|
|
|
|
/* Subscribe to events */
|
|
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);
|
|
delegate().subscribe(&Cakefoot::respond, this, SDL_JOYAXISMOTION);
|
|
delegate().subscribe(&Cakefoot::respond, this, SDL_JOYBUTTONDOWN);
|
|
delegate().subscribe(&Cakefoot::respond, this, SDL_JOYBUTTONUP);
|
|
delegate().subscribe(&Cakefoot::respond, this, SDL_KEYDOWN);
|
|
|
|
/* Open a game controller if any are available at the beginning of the program */
|
|
open_game_controller();
|
|
|
|
/* 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();
|
|
|
|
/* Link shaders and uniforms */
|
|
initialize_gl();
|
|
|
|
/* 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"));
|
|
|
|
/* Set up checkpoint on and off sprites */
|
|
checkpoint_on = sb::Sprite {"resource/checkpoint/on.png", glm::vec2(12.0f / 486.0f), GL_LINEAR};
|
|
checkpoint_off = sb::Sprite {"resource/checkpoint/off.png", glm::vec2(12.0f / 486.0f), GL_LINEAR};
|
|
|
|
/* Set hitbox */
|
|
character.box_size(configuration()("character", "hitbox").get<float>());
|
|
|
|
/* Load coin graphics */
|
|
coin.load();
|
|
|
|
/* Load splash screens */
|
|
for (nlohmann::json splash_config : configuration()("display", "splash"))
|
|
{
|
|
splash.emplace_back(Splash {
|
|
sb::Sprite{splash_config.at(0).get<std::string>(), glm::vec2{1.77777f, 1.0f}, GL_LINEAR},
|
|
splash_config.at(1).get<glm::vec4>(),
|
|
splash_config.at(2)});
|
|
}
|
|
|
|
/* Load SFX and BGM */
|
|
load_audio();
|
|
|
|
/* Set value from configuration once at construction so it doesn't change at runtime and break the program */
|
|
use_play_button = configuration()("display", "use play button");
|
|
|
|
/* Set to default values in case these get displayed erroneously */
|
|
label.at("arcade rank").content("999th");
|
|
label.at("arcade rank").refresh();
|
|
label.at("arcade distance").content("10000m");
|
|
label.at("arcade distance").refresh();
|
|
|
|
/* Initialize scoreboard content */
|
|
refresh_scoreboard();
|
|
|
|
/* Start tracking hue rotation */
|
|
shift_hue_animation.frame_length(configuration()("display", "hue shift frequency"));
|
|
shift_hue_animation.play();
|
|
|
|
/* Start screen effect animations */
|
|
blink_animation.play();
|
|
warning_animation.play();
|
|
|
|
if (!use_play_button)
|
|
{
|
|
if (splash.size() > 0)
|
|
{
|
|
splash_index = 0;
|
|
splash_animation.play_once(splash[0].length);
|
|
world_color = splash[0].background.normal();
|
|
set_up_buttons();
|
|
}
|
|
else
|
|
{
|
|
/* Load title screen */
|
|
load_level(0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Just set up buttons because only play and volume buttons are needed */
|
|
set_up_buttons();
|
|
}
|
|
|
|
/* Switch volume on */
|
|
button.at("volume").press();
|
|
|
|
/* Track idle time */
|
|
idle_timer.on();
|
|
|
|
#if defined(EMSCRIPTEN)
|
|
/* Pause the game when the browser tab is hidden */
|
|
if (emscripten_set_visibilitychange_callback(this, false, &respond_to_visibility_change) < 0)
|
|
{
|
|
sb::Log::log("Failed to enable browser visibility change automatic pause feature", sb::Log::WARN);
|
|
}
|
|
|
|
/* Open the game controller when it is connected */
|
|
if (emscripten_set_gamepadconnected_callback(this, false, &respond_to_gamepad_connected) < 0)
|
|
{
|
|
sb::Log::log("Failed to listen for gamepad connections", sb::Log::WARN);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Cakefoot::open_game_controller()
|
|
{
|
|
for (int ii = 0, jj = 0; ii < SDL_NumJoysticks(); ii++)
|
|
{
|
|
if (SDL_IsGameController(ii))
|
|
{
|
|
std::ostringstream message;
|
|
message << "Gamepad #" << ++jj << ": ";
|
|
std::string name {SDL_GameControllerNameForIndex(ii)};
|
|
if (name == "")
|
|
{
|
|
name = "[unnamed]";
|
|
}
|
|
message << name;
|
|
sb::Log::log(message);
|
|
if (controller.get() == nullptr)
|
|
{
|
|
controller = std::shared_ptr<SDL_GameController>(SDL_GameControllerOpen(ii), SDL_GameControllerClose);
|
|
std::ostringstream message;
|
|
if (controller.get() == nullptr)
|
|
{
|
|
message << "Could not open gamepad #" << jj;
|
|
sb::Log::sdl_error(message.str());
|
|
}
|
|
else
|
|
{
|
|
message << "Opened gamepad #" << jj;
|
|
sb::Log::log(message);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::ostringstream message;
|
|
message << "Joystick #" << ii << " cannot be loaded by the game controller API";
|
|
sb::Log::log(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Cakefoot::load_audio()
|
|
{
|
|
audio = {};
|
|
for (const auto& [name, path] : configuration()("audio", "files").items())
|
|
{
|
|
std::ostringstream message;
|
|
message << "Loading audio at " << path.get<std::string>();
|
|
sb::Log::log(message);
|
|
audio[name] = sb::audio::Chunk(path.get<std::string>());
|
|
}
|
|
|
|
/* 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::initialize_gl()
|
|
{
|
|
/* 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(configuration()("shader", "vertex"), GL_VERTEX_SHADER);
|
|
GLuint fragment_shader = load_shader(configuration()("shader", "fragment"), 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",
|
|
"challenge increment", "challenge decrement", "view increment", "view decrement"
|
|
})
|
|
{
|
|
sb::Text text {name == "resume" || name == "reset" ? fonts.at("medium") : fonts.at("glyph")};
|
|
float scale;
|
|
glm::vec2 dimensions;
|
|
if (name == "start" || name == "resume" || name == "reset")
|
|
{
|
|
dimensions = glm::vec2{configuration()("button", "text dimensions")};
|
|
scale = configuration()("button", "text scale");
|
|
}
|
|
else
|
|
{
|
|
dimensions = glm::vec2{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);
|
|
text.refresh();
|
|
button.at(name) = sb::Pad<>{text, configuration()("button", name + " translation"), scale, dimensions.y / dimensions.x};
|
|
}
|
|
|
|
/* Replace start text texture with arcade prompt image if requested in config */
|
|
if (configuration()("display", "use arcade prompt"))
|
|
{
|
|
sb::Texture arcade_prompt_texture {configuration()("button", "start alt texture").get<std::string>()};
|
|
arcade_prompt_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
sb::Plane arcade_prompt_plane;
|
|
arcade_prompt_plane.texture(arcade_prompt_texture);
|
|
button.at("start") = sb::Pad<>{arcade_prompt_plane, configuration()("button", "start alt translation"),
|
|
configuration()("button", "start alt scale")[1], 1.0f / 5.0f};
|
|
}
|
|
|
|
/* Set up text button callbacks */
|
|
button.at("start").on_state_change([&](bool state){
|
|
if (configuration()("challenge", challenge_index, "name") == "NEW QUEST")
|
|
{
|
|
configuration()["progress"]["quest level"] = 1;
|
|
configuration()["progress"]["quest checkpoint"] = 0.0f;
|
|
configuration()["progress"]["quest difficulty"] = profile_index;
|
|
configuration()["progress"]["quest time"] = 0.0f;
|
|
configuration()["progress"]["quest bank"] = 0;
|
|
challenge_index = 0;
|
|
configuration()["progress"]["current challenge"] = challenge_index;
|
|
}
|
|
else if (configuration()("challenge", challenge_index, "name") == "ARCADE")
|
|
{
|
|
configuration()["progress"]["arcade level"] = 1;
|
|
configuration()["progress"]["arcade checkpoint"] = 0.0f;
|
|
configuration()["progress"]["arcade difficulty"] = profile_index;
|
|
configuration()["progress"]["arcade max distance"] = 0;
|
|
configuration()["progress"]["arcade time"] = 0.0f;
|
|
configuration()["progress"]["arcade bank"] = 0;
|
|
challenge_index = 3;
|
|
configuration()["progress"]["current challenge"] = challenge_index;
|
|
}
|
|
#if defined(__COOLMATH__)
|
|
/* Coolmath API */
|
|
EM_ASM( if (parent.cmgGameEvent !== undefined) { console.log("cmgGameEvent start"); parent.cmgGameEvent("start"); } );
|
|
#endif
|
|
load_level(level_select_index);
|
|
});
|
|
button.at("resume").on_state_change([&](bool state){
|
|
sb::Delegate::post("resume", false);
|
|
});
|
|
button.at("reset").on_state_change([&](bool state){
|
|
sb::Delegate::post(reset_command_name, false);
|
|
});
|
|
|
|
/* Set up pause button */
|
|
sb::Texture pause_texture {configuration()("button", "pause texture").get<std::string>()};
|
|
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){
|
|
sb::Delegate::post("pause", false);
|
|
});
|
|
|
|
/* Set up volume button */
|
|
bool original_state = button.at("volume").pressed();
|
|
sb::Texture volume_off_texture {configuration()("button", "volume off texture").get<std::string>()};
|
|
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").get<std::string>()};
|
|
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){
|
|
/* Mute or unmute (to full volume) depending on the state of the button */
|
|
if (Mix_QuerySpec(nullptr, nullptr, nullptr) != 0)
|
|
{
|
|
if (state)
|
|
{
|
|
Mix_Volume(-1, 128);
|
|
}
|
|
else
|
|
{
|
|
Mix_Volume(-1, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb::Log::log("Cannot mute or unmute. Audio device is not open.", sb::Log::WARN);
|
|
}
|
|
});
|
|
|
|
/* Set up spinners */
|
|
for (const std::string& name : {"profile", "level select", "challenge", "view"})
|
|
{
|
|
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;
|
|
}
|
|
else if (name == "challenge")
|
|
{
|
|
message << configuration()("challenge", challenge_index, "name").get<std::string>();
|
|
}
|
|
else if (name == "view")
|
|
{
|
|
message << configuration()("view", view_index, "name").get<std::string>();
|
|
}
|
|
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);
|
|
label.at(name).refresh();
|
|
}
|
|
button.at("profile decrement").on_state_change([&](bool state){
|
|
/* Disable in arcade-only mode and resume game modes */
|
|
if (!configuration()("display", "arcade only") && configuration()("challenge", challenge_index, "name") != "RESUME QUEST" &&
|
|
configuration()("challenge", challenge_index, "name") != "RESUME ARCADE")
|
|
{
|
|
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){
|
|
/* Disable in arcade-only mode and resume game modes */
|
|
if (!configuration()("display", "arcade only") && configuration()("challenge", challenge_index, "name") != "RESUME QUEST" &&
|
|
configuration()("challenge", challenge_index, "name") != "RESUME ARCADE")
|
|
{
|
|
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){
|
|
/* Only allow level select in level select mode */
|
|
if (configuration()("challenge", challenge_index, "name") == "LEVEL SELECT")
|
|
{
|
|
/* 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");
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Save state and redraw button */
|
|
configuration()["progress"]["current level"] = level_select_index;
|
|
write_progress();
|
|
set_up_buttons();
|
|
});
|
|
button.at("level increment").on_state_change([&](bool state){
|
|
|
|
/* Only allow level select in level select mode */
|
|
if (configuration()("challenge", challenge_index, "name") == "LEVEL SELECT")
|
|
{
|
|
/* 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;
|
|
}
|
|
}
|
|
|
|
/* Save state and redraw button */
|
|
configuration()["progress"]["current level"] = level_select_index;
|
|
write_progress();
|
|
set_up_buttons();
|
|
});
|
|
button.at("challenge decrement").on_state_change([&](bool state){
|
|
/* Only allow change if arcade-only mode is not active */
|
|
if (!configuration()("display", "arcade only"))
|
|
{
|
|
if (--challenge_index < 0) challenge_index = configuration()("progress", "max challenge");
|
|
if (skip_resume_quest() || skip_resume_arcade() || skip_level_select())
|
|
{
|
|
button.at("challenge decrement").press();
|
|
}
|
|
else
|
|
{
|
|
toggle_challenge();
|
|
}
|
|
}
|
|
});
|
|
button.at("challenge increment").on_state_change([&](bool state){
|
|
/* Only allow change if arcade-only mode is not active */
|
|
if (!configuration()("display", "arcade only"))
|
|
{
|
|
if (++challenge_index > configuration()("progress", "max challenge")) challenge_index = 0;
|
|
if (skip_resume_quest() || skip_resume_arcade() || skip_level_select())
|
|
{
|
|
button.at("challenge increment").press();
|
|
}
|
|
else
|
|
{
|
|
toggle_challenge();
|
|
}
|
|
}
|
|
});
|
|
button.at("view decrement").on_state_change([&](bool state){
|
|
if (--view_index < 0)
|
|
{
|
|
view_index = configuration()("progress", "max view");
|
|
}
|
|
configuration()["progress"]["current view"] = view_index;
|
|
write_progress();
|
|
set_up_buttons();
|
|
});
|
|
button.at("view increment").on_state_change([&](bool state){
|
|
if (++view_index > configuration()("progress", "max view"))
|
|
{
|
|
view_index = 0;
|
|
}
|
|
configuration()["progress"]["current view"] = view_index;
|
|
write_progress();
|
|
set_up_buttons();
|
|
});
|
|
|
|
/* Set up play button */
|
|
original_state = button.at("play").pressed();
|
|
sb::Texture play_texture {configuration()("button", "play texture").get<std::string>()};
|
|
play_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
sb::Plane play_plane;
|
|
play_plane.texture(play_texture);
|
|
button.at("play") = sb::Pad<>{play_plane, configuration()("button", "play translation"), configuration()("button", "play scale"),
|
|
configuration()("button", "play scale ratio")};
|
|
button.at("play").state(original_state);
|
|
button.at("play").on_state_change([&](bool state){
|
|
if (splash.size() > 0)
|
|
{
|
|
splash_index = 0;
|
|
splash_animation.play_once(splash[0].length);
|
|
world_color = splash[0].background.normal();
|
|
}
|
|
else
|
|
{
|
|
load_level(0);
|
|
}
|
|
});
|
|
|
|
/* Set up name entry buttons */
|
|
for (const std::string& character_index : {"1", "2", "3"})
|
|
{
|
|
glm::vec2 character_dimensions {configuration()("button", "name", "character dimensions")};
|
|
sb::Text character {fonts.at("large"), "", configuration()("display", "clock hud foreground").get<glm::vec4>(),
|
|
configuration()("display", "clock hud background").get<glm::vec4>(), character_dimensions};
|
|
character.content(name_entry[std::stoi(character_index) - 1]);
|
|
character.refresh();
|
|
button.at("name " + character_index) = sb::Pad<>{
|
|
character, {configuration()("button", "name", "character " + character_index + " x"), configuration()("button", "name", "character y")},
|
|
configuration()("button", "name", "character scale")[1], character_dimensions.y / character_dimensions.x};
|
|
sb::Texture increment_texture {configuration()("button", "name", "arrow increment texture").get<std::string>()};
|
|
increment_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
sb::Plane increment_plane;
|
|
increment_plane.texture(increment_texture);
|
|
glm::vec2 arrow_dimensions {configuration()("button", "name", "arrow dimensions")};
|
|
button.at("name " + character_index + " increment") = sb::Pad<>{
|
|
increment_plane, {configuration()("button", "name", "character " + character_index + " x"), configuration()("button", "name", "arrow increment y")},
|
|
configuration()("button", "name", "arrow scale")[1], arrow_dimensions.y / arrow_dimensions.x};
|
|
sb::Texture decrement_texture {configuration()("button", "name", "arrow decrement texture").get<std::string>()};
|
|
decrement_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
sb::Plane decrement_plane;
|
|
decrement_plane.texture(decrement_texture);
|
|
button.at("name " + character_index + " decrement") = sb::Pad<>{
|
|
decrement_plane, {configuration()("button", "name", "character " + character_index + " x"), configuration()("button", "name", "arrow decrement y")},
|
|
configuration()("button", "name", "arrow scale")[1], arrow_dimensions.y / arrow_dimensions.x};
|
|
button.at("name " + character_index).on_state_change([&, character_index](bool state){
|
|
name_entry_index = std::stoi(character_index) - 1;
|
|
});
|
|
button.at("name " + character_index + " increment").on_state_change([&, character_index, character_dimensions](bool state){
|
|
char current = name_entry[std::stoi(character_index) - 1];
|
|
if (++current > 'Z')
|
|
{
|
|
current = 'A';
|
|
}
|
|
name_entry[std::stoi(character_index) - 1] = current;
|
|
set_up_buttons();
|
|
});
|
|
button.at("name " + character_index + " decrement").on_state_change([&, character_index, character_dimensions](bool state){
|
|
char current = name_entry[std::stoi(character_index) - 1];
|
|
if (--current < 'A')
|
|
{
|
|
current = 'Z';
|
|
}
|
|
name_entry[std::stoi(character_index) - 1] = current;
|
|
set_up_buttons();
|
|
});
|
|
if (!configuration()("display", "name entry enabled"))
|
|
{
|
|
button.at("name " + character_index + " increment").enabled(false);
|
|
button.at("name " + character_index + " increment").visible(false);
|
|
button.at("name " + character_index + " decrement").enabled(false);
|
|
button.at("name " + character_index + " decrement").visible(false);
|
|
button.at("name " + character_index).enabled(false);
|
|
}
|
|
}
|
|
|
|
/* Set up fullscreen button */
|
|
sb::Texture fullscreen_texture {configuration()("button", "fullscreen texture").get<std::string>()};
|
|
fullscreen_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
sb::Plane fullscreen_plane;
|
|
fullscreen_plane.texture(fullscreen_texture);
|
|
button.at("fullscreen") = sb::Pad<>{fullscreen_plane, configuration()("button", "fullscreen translation"), configuration()("button", "fullscreen scale"),
|
|
configuration()("button", "fullscreen scale ratio")};
|
|
button.at("fullscreen").on_state_change([&](bool state){
|
|
display.toggle_fullscreen();
|
|
});
|
|
|
|
/* Set up social buttons */
|
|
sb::Texture diskmem_texture {configuration()("display", "social diskmem texture").get<std::string>()};
|
|
diskmem_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
sb::Plane diskmem_plane;
|
|
diskmem_plane.texture(diskmem_texture);
|
|
button.at("diskmem") = sb::Pad<>{diskmem_plane, configuration()("display", "social diskmem translation"), configuration()("display", "social single scale"),
|
|
configuration()("display", "social single ratio")};
|
|
button.at("diskmem").on_state_change([&](bool state){
|
|
#if defined(EMSCRIPTEN) && !defined(__COOLMATH__)
|
|
EM_ASM(
|
|
{
|
|
Object.assign(document.createElement('a'), {
|
|
target: '_blank',
|
|
rel: 'noopener noreferrer',
|
|
href: UTF8ToString($0),
|
|
}).click();
|
|
}, configuration()("display", "social diskmem url").get<std::string>().c_str());
|
|
#endif
|
|
});
|
|
sb::Texture azuria_sky_texture {configuration()("display", "social azuria sky texture").get<std::string>()};
|
|
azuria_sky_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
sb::Plane azuria_sky_plane;
|
|
azuria_sky_plane.texture(azuria_sky_texture);
|
|
button.at("azuria sky") = sb::Pad<>{
|
|
azuria_sky_plane, configuration()("display", "social azuria sky translation"), configuration()("display", "social single scale"),
|
|
configuration()("display", "social single ratio")};
|
|
button.at("azuria sky").on_state_change([&](bool state){
|
|
#if defined(EMSCRIPTEN) && !defined(__COOLMATH__)
|
|
EM_ASM(
|
|
{
|
|
Object.assign(document.createElement('a'), {
|
|
target: '_blank',
|
|
rel: 'noopener noreferrer',
|
|
href: UTF8ToString($0),
|
|
}).click();
|
|
}, configuration()("display", "social azuria sky url").get<std::string>().c_str());
|
|
#endif
|
|
});
|
|
}
|
|
|
|
void Cakefoot::toggle_challenge()
|
|
{
|
|
/* In resume modes, set the level select and difficulty to the saved values. */
|
|
if (configuration()("challenge", challenge_index, "name") == "RESUME QUEST")
|
|
{
|
|
level_select_index = configuration()("progress", "quest level").get<int>();
|
|
profile_index = configuration()("progress", "quest difficulty").get<int>();
|
|
configuration()["progress"]["current difficulty"] = profile_index;
|
|
character.profile(configuration()("character", "profile", profile_index, "name"));
|
|
}
|
|
else if (configuration()("challenge", challenge_index, "name") == "RESUME ARCADE")
|
|
{
|
|
level_select_index = configuration()("progress", "arcade level").get<int>();
|
|
profile_index = configuration()("progress", "arcade difficulty").get<int>();
|
|
configuration()["progress"]["current difficulty"] = profile_index;
|
|
character.profile(configuration()("character", "profile", profile_index, "name"));
|
|
}
|
|
|
|
/* In new game modes, set the level select to 1 and leave the difficulty unchanged. */
|
|
else if (configuration()("challenge", challenge_index, "name") == "ARCADE" ||
|
|
configuration()("challenge", challenge_index, "name") == "NEW QUEST")
|
|
{
|
|
level_select_index = 1;
|
|
}
|
|
|
|
configuration()["progress"]["current challenge"] = challenge_index;
|
|
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);
|
|
|
|
glm::vec3 clock_scale, clock_translation;
|
|
if (static_cast<std::size_t>(level_index) == _configuration("levels").size() - 1)
|
|
{
|
|
label.at("clock").font(fonts.at("large"));
|
|
if (arcade())
|
|
{
|
|
/* Arcade results size */
|
|
clock_scale = configuration()("display", "arcade time remaining scale");
|
|
clock_translation = configuration()("display", "arcade time remaining translation");
|
|
}
|
|
else
|
|
{
|
|
/* Quest results size */
|
|
clock_scale = configuration()("display", "clock hud large scale");
|
|
clock_translation = configuration()("display", "clock hud large translation");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
label.at("clock").font(font());
|
|
/* Standard HUD size */
|
|
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"));
|
|
|
|
/* Style the game over text */
|
|
label.at("game over").content(configuration()("display", "game over text"));
|
|
label.at("game over").foreground(configuration()("display", "game over foreground").get<glm::vec4>());
|
|
label.at("game over").background(configuration()("display", "game over background").get<glm::vec4>());
|
|
label.at("game over").untransform();
|
|
label.at("game over").translate(configuration()("display", "game over translation"));
|
|
label.at("game over").scale(configuration()("display", "game over scale"));
|
|
label.at("game over").refresh();
|
|
|
|
/* Style arcade results */
|
|
for (const std::string& name : {"arcade rank", "arcade distance"})
|
|
{
|
|
label.at(name).foreground(configuration()("display", "clock hud foreground").get<glm::vec4>());
|
|
label.at(name).background(configuration()("display", "clock hud background").get<glm::vec4>());
|
|
label.at(name).untransform();
|
|
label.at(name).translate(configuration()("display", name + " translation"));
|
|
label.at(name).scale(configuration()("display", name + " scale"));
|
|
label.at(name).dimensions(configuration()("display", name + " dimensions"));
|
|
label.at(name).refresh();
|
|
}
|
|
|
|
/* Style the scoreboard */
|
|
scoreboard.wrap(configuration()("display", "scoreboard wrap"));
|
|
scoreboard.foreground(configuration()("display", "scoreboard foreground").get<glm::vec4>());
|
|
scoreboard.background(configuration()("display", "scoreboard background").get<glm::vec4>());
|
|
scoreboard.refresh();
|
|
scoreboard.untransform();
|
|
scoreboard.translate(configuration()("display", "scoreboard translation"));
|
|
scoreboard.scale(configuration()("display", "scoreboard scale"));
|
|
|
|
/* Style the QR code */
|
|
sb::Texture qr_texture {configuration()("display", "qr texture").get<std::string>()};
|
|
qr_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
qr_code.texture(qr_texture);
|
|
qr_code.translate(configuration()("display", "qr translation"));
|
|
qr_code.scale(configuration()("display", "qr scale"));
|
|
sb::Texture qr_bg_texture {configuration()("display", "qr background texture").get<std::string>()};
|
|
qr_bg_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
qr_code_bg.texture(qr_bg_texture);
|
|
qr_code_bg.translate(configuration()("display", "qr translation"));
|
|
qr_code_bg.scale(configuration()("display", "qr scale"));
|
|
|
|
/* Style the social buttons */
|
|
sb::Texture social_texture {configuration()("display", "social texture").get<std::string>()};
|
|
social_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
social.texture(social_texture);
|
|
std::string social_translation;
|
|
if (configuration()("display", "qr display"))
|
|
{
|
|
if (configuration()("display", "arcade only"))
|
|
{
|
|
social_translation = "social arcade translation";
|
|
}
|
|
else
|
|
{
|
|
social_translation = "social qr translation";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
social_translation = "social web translation";
|
|
}
|
|
social.translate(configuration()("display", social_translation));
|
|
social.scale(configuration()("display", "social scale"));
|
|
|
|
/* Set up auto save icon */
|
|
auto_save = sb::Sprite {configuration()("texture", "auto save").get<std::string>(),
|
|
configuration()("display", "auto save scale").get<glm::vec2>(), GL_LINEAR};
|
|
auto_save.translate(configuration()("display", "auto save translation"));
|
|
|
|
/* Style the quest best time indicator */
|
|
label.at("quest best").foreground(configuration()("display", "quest best foreground").get<glm::vec4>());
|
|
label.at("quest best").background(configuration()("display", "quest best background").get<glm::vec4>());
|
|
label.at("quest best").untransform();
|
|
label.at("quest best").translate(configuration()("display", "quest best translation"));
|
|
label.at("quest best").scale(configuration()("display", "quest best scale"));
|
|
label.at("quest best").dimensions(configuration()("display", "quest best dimensions"));
|
|
if (configuration()("progress", "quest best") > 0.0f)
|
|
{
|
|
label.at("quest best").content(configuration()("display", "quest best text").get<std::string>() +
|
|
format_clock(configuration()("progress", "quest best")));
|
|
label.at("quest best").refresh();
|
|
}
|
|
|
|
/* Style the playtester thanks text */
|
|
thanks.wrap(configuration()("ending", "thanks wrap"));
|
|
thanks.content(configuration()("ending", "thanks"));
|
|
thanks.foreground(configuration()("ending", "messages foreground").get<glm::vec4>());
|
|
thanks.untransform();
|
|
thanks.translate(configuration()("ending", "thanks translation"));
|
|
thanks.scale(configuration()("ending", "thanks scale"));
|
|
thanks.refresh();
|
|
|
|
/* Style the idle warning */
|
|
label.at("idle warning").content(configuration()("demo", "countdown message"));
|
|
label.at("idle warning").foreground(configuration()("display", "idle warning foreground").get<glm::vec4>());
|
|
label.at("idle warning").background(configuration()("display", "idle warning background").get<glm::vec4>());
|
|
label.at("idle warning").untransform();
|
|
label.at("idle warning").translate(configuration()("display", "idle warning translation"));
|
|
label.at("idle warning").scale(configuration()("display", "idle warning scale"));
|
|
label.at("idle warning").refresh();
|
|
|
|
/* Style the demo message */
|
|
sb::Texture demo_message_texture {configuration()("texture", "demo message").get<std::string>()};
|
|
demo_message_texture.load();
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
demo_message.texture(demo_message_texture);
|
|
demo_message.translate(configuration()("demo", "message translation"));
|
|
demo_message.scale(configuration()("demo", "message 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);
|
|
}
|
|
|
|
const Curve& Cakefoot::curve() const
|
|
{
|
|
return curves[curve_index % curves.size()];
|
|
}
|
|
|
|
Curve& Cakefoot::curve()
|
|
{
|
|
return curves[curve_index % curves.size()];
|
|
}
|
|
|
|
void Cakefoot::load_level(int index)
|
|
{
|
|
/* Cancel selection */
|
|
selected.reset();
|
|
|
|
/* Wrap the index if it is out of range. */
|
|
index = glm::mod(index, static_cast<int>(configuration()("levels").size()));
|
|
|
|
/* Play menu theme on title screen and end screen. Play main theme on any other level. Cross fade between the two. */
|
|
if (index == 0 || static_cast<std::size_t>(index) == configuration()("levels").size() - 1)
|
|
{
|
|
/* If menu theme is already playing, let it continue to play. */
|
|
if (!audio.at("menu").playing() || audio.at("menu").paused() || audio.at("menu").fading())
|
|
{
|
|
audio.at("menu").play();
|
|
}
|
|
|
|
audio.at("main").stop();
|
|
}
|
|
else
|
|
{
|
|
if (audio.at("main").paused() || !audio.at("main").playing())
|
|
{
|
|
audio.at("main").play();
|
|
}
|
|
|
|
audio.at("menu").stop();
|
|
}
|
|
|
|
/* Update indices and reset character. */
|
|
level_index = index;
|
|
curve_index = index;
|
|
character.beginning(curve());
|
|
coin_collected = false;
|
|
|
|
/* 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};
|
|
|
|
/* Time out end screen */
|
|
if (static_cast<std::size_t>(index) == configuration()("levels").size() - 1)
|
|
{
|
|
submit_score_animation.play_once(configuration()("display", "end screen timeout"));
|
|
}
|
|
else
|
|
{
|
|
submit_score_animation.reset();
|
|
}
|
|
|
|
/* 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 the level is the end screen, reset the player's current level to the beginning and load ending screen coin list. Unlock
|
|
* any new difficulty or view. Set a list of messages to be displayed on the end screen. */
|
|
if (end_screen())
|
|
{
|
|
/* Load ending coins */
|
|
std::string coin_texture;
|
|
ending_coins.clear();
|
|
glm::vec2 coin_range = configuration()("ending", "coin range").get<glm::vec2>();
|
|
float coin_step = (coin_range.y - coin_range.x) / (bank() - 1);
|
|
for (std::size_t ii = 0; ii < bank(); ii++)
|
|
{
|
|
Flame coin {
|
|
field, glm::vec3{coin_range.x + coin_step * ii, configuration()("ending", "coin y").get<float>(), 0.0f},
|
|
0.0f, 0.0f, -1.0f, bank() < max_bank()
|
|
};
|
|
ending_coins.push_back(coin);
|
|
}
|
|
|
|
/* Clear list of ending messages */
|
|
ending_messages.clear();
|
|
|
|
/* Show the end for any run that beats all the levels */
|
|
if (quest() || (arcade() && configuration()("progress", "arcade level") >= configuration()("levels").size() - 2))
|
|
{
|
|
sb::Text message {fonts.at("glyph large"), configuration()("ending", "end text"),
|
|
configuration()("ending", "messages foreground").get<glm::vec4>()};
|
|
message.dimensions(configuration()("ending", "messages dimensions"));
|
|
message.refresh();
|
|
ending_messages.push_back(message);
|
|
}
|
|
|
|
/* Unlocks for getting all coins */
|
|
if (bank() >= max_bank())
|
|
{
|
|
if (configuration()("progress", "max view").get<int>() < 1)
|
|
{
|
|
configuration()["progress"]["max view"] = 1;
|
|
sb::Text message {fonts.at("glyph"), configuration()("ending", "unlock mirror"),
|
|
configuration()("ending", "messages foreground").get<glm::vec4>()};
|
|
message.dimensions(configuration()("ending", "messages dimensions"));
|
|
message.refresh();
|
|
ending_messages.push_back(message);
|
|
}
|
|
if (configuration()("progress", "max view").get<int>() < 2 && profile_index >= 1)
|
|
{
|
|
configuration()["progress"]["max view"] = 2;
|
|
sb::Text message {fonts.at("glyph"), configuration()("ending", "unlock warped"),
|
|
configuration()("ending", "messages foreground").get<glm::vec4>()};
|
|
message.dimensions(configuration()("ending", "messages dimensions"));
|
|
message.refresh();
|
|
ending_messages.push_back(message);
|
|
}
|
|
if (configuration()("progress", "jackpot") != 777 && profile_index == 2)
|
|
{
|
|
configuration()["progress"]["jackpot"] = 777;
|
|
character.profile(configuration()("character", "profile", profile_index, "name"));
|
|
sb::Text message {fonts.at("glyph"), configuration()("ending", "unlock jackpot"),
|
|
configuration()("ending", "messages foreground").get<glm::vec4>()};
|
|
message.dimensions(configuration()("ending", "messages dimensions"));
|
|
message.refresh();
|
|
ending_messages.push_back(message);
|
|
}
|
|
}
|
|
|
|
configuration()["progress"]["current level"] = 1;
|
|
|
|
/* Update save progress */
|
|
if (arcade())
|
|
{
|
|
configuration()["progress"]["arcade level"] = 1;
|
|
configuration()["progress"]["arcade checkpoint"] = 0.0f;
|
|
configuration()["progress"]["arcade max distance"] = 0;
|
|
configuration()["progress"]["arcade time"] = 0.0f;
|
|
configuration()["progress"]["arcade bank"] = 0;
|
|
challenge_index = 4;
|
|
configuration()["progress"]["current challenge"] = challenge_index;
|
|
}
|
|
else if (quest())
|
|
{
|
|
configuration()["progress"]["quest level"] = 1;
|
|
configuration()["progress"]["quest checkpoint"] = 0.0f;
|
|
configuration()["progress"]["quest time"] = 0.0f;
|
|
configuration()["progress"]["quest bank"] = 0;
|
|
challenge_index = 1;
|
|
configuration()["progress"]["current challenge"] = challenge_index;
|
|
}
|
|
|
|
/* In quest mode, unlock higher difficulty at the end of the game: 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. Save
|
|
* the time if it is better than the existing record. */
|
|
if (quest())
|
|
{
|
|
if (profile_index < static_cast<int>(configuration()("character", "profile").size()) - 1)
|
|
{
|
|
if (profile_index == configuration()("progress", "max level"))
|
|
{
|
|
sb::Text message {fonts.at("glyph")};
|
|
message.foreground(configuration()("ending", "messages foreground").get<glm::vec4>());
|
|
profile_index == 1 ? message.content(configuration()("ending", "unlock beef")) :
|
|
message.content(configuration()("ending", "unlock buffalo"));
|
|
message.dimensions(configuration()("ending", "messages dimensions"));
|
|
message.refresh();
|
|
ending_messages.push_back(message);
|
|
}
|
|
|
|
configuration()["progress"]["max difficulty"] = ++profile_index;
|
|
configuration()["progress"]["max level"] = 1;
|
|
character.profile(configuration()("character", "profile", profile_index, "name"));
|
|
}
|
|
|
|
float best = configuration()("progress", "quest best");
|
|
if (best <= 0.0f || run_timer.elapsed() < best)
|
|
{
|
|
configuration()["progress"]["quest best"] = run_timer.elapsed();
|
|
label.at("quest best").content(
|
|
configuration()("display", "quest best text").get<std::string>() + " " + format_clock(run_timer.elapsed()));
|
|
label.at("quest best").refresh();
|
|
sb::Text message {fonts.at("glyph"), configuration()("ending", "new best"),
|
|
configuration()("ending", "messages foreground").get<glm::vec4>()};
|
|
message.dimensions(configuration()("ending", "messages dimensions"));
|
|
message.refresh();
|
|
ending_messages.push_back(message);
|
|
}
|
|
}
|
|
|
|
write_progress();
|
|
level_select_index = 1;
|
|
}
|
|
|
|
/* 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. */
|
|
else if (index > 0)
|
|
{
|
|
/* Unlock the level if it is a newly reached level */
|
|
if (configuration()("progress", "max difficulty") == profile_index &&
|
|
configuration()("progress", "max level").get<int>() < index)
|
|
{
|
|
configuration()["progress"]["max level"] = index;
|
|
}
|
|
|
|
/* Read and write save progress */
|
|
if (arcade())
|
|
{
|
|
if (configuration()("progress", "arcade level") == index)
|
|
{
|
|
/* If resuming, set the clock and checkpoint */
|
|
run_timer.elapsed(configuration()("progress", "arcade time").get<float>());
|
|
character.checkpoint(configuration()("progress", "arcade checkpoint").get<float>());
|
|
character.spawn(curve());
|
|
if (configuration()("progress", "arcade coin"))
|
|
{
|
|
for (auto& enemy : enemies) enemy->take_coin();
|
|
collect_coin(false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
configuration()["progress"]["arcade level"] = index;
|
|
configuration()["progress"]["arcade checkpoint"] = 0;
|
|
configuration()["progress"]["arcade difficulty"] = profile_index;
|
|
configuration()["progress"]["arcade max distance"] = distance();
|
|
configuration()["progress"]["arcade coin"] = false;
|
|
}
|
|
}
|
|
else if (quest())
|
|
{
|
|
if (configuration()("progress", "quest level") == index)
|
|
{
|
|
/* If resuming, set the checkpoint and clock */
|
|
run_timer.elapsed(configuration()("progress", "quest time").get<float>());
|
|
character.checkpoint(configuration()("progress", "quest checkpoint").get<float>());
|
|
character.spawn(curve());
|
|
if (configuration()("progress", "quest coin"))
|
|
{
|
|
for (auto& enemy : enemies) enemy->take_coin();
|
|
collect_coin(false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
configuration()["progress"]["quest level"] = index;
|
|
configuration()["progress"]["quest checkpoint"] = 0;
|
|
configuration()["progress"]["quest difficulty"] = profile_index;
|
|
configuration()["progress"]["quest coin"] = false;
|
|
}
|
|
}
|
|
|
|
configuration()["progress"]["current level"] = index;
|
|
write_progress();
|
|
level_select_index = index;
|
|
}
|
|
|
|
/* If it's the title or end level, stop the run timer. */
|
|
if (index == 0 || end_screen())
|
|
{
|
|
run_timer.off();
|
|
arcade_limit_warning = false;
|
|
|
|
/* In arcade mode, reset the clock on the title screen */
|
|
if (arcade() && index == 0)
|
|
{
|
|
run_timer.reset();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* If it's the first level, start the run timer from the beginning. */
|
|
if (index == 1)
|
|
{
|
|
run_timer.reset();
|
|
}
|
|
run_timer.on();
|
|
}
|
|
|
|
/* In demo mode, reset the challenge to new quest every time the title is loaded */
|
|
if (configuration()("demo", "active") && index == 0)
|
|
{
|
|
challenge_index = 1;
|
|
level_select_index = 1;
|
|
}
|
|
|
|
/* 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. Use world 0 color for the end screen. */
|
|
nlohmann::json world = configuration()("world");
|
|
if (static_cast<std::size_t>(index) == configuration()("levels").size() - 1)
|
|
{
|
|
world_color = world[0].at("color").get<glm::fvec4>();
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
} } }
|
|
|
|
/* Flash the screen at the start of a level */
|
|
flash_animation.play_once(configuration()("display", "flash length"));
|
|
|
|
#if defined(__COOLMATH__)
|
|
/* Send a game event to the Coolmath API if it's a regular level. */
|
|
if (index > 0 && static_cast<std::size_t>(index) < configuration()("levels").size() - 1)
|
|
{
|
|
EM_ASM(
|
|
{
|
|
if (parent.cmgGameEvent !== undefined)
|
|
{
|
|
console.log("cmgGameEvent start " + $0);
|
|
parent.cmgGameEvent("start", $0);
|
|
}
|
|
}, index);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Cakefoot::write_progress() const
|
|
{
|
|
/* Create directory for save files */
|
|
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::ERR);
|
|
}
|
|
}
|
|
|
|
/* Save player's progress file */
|
|
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::ERR);
|
|
}
|
|
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::write_scores() const
|
|
{
|
|
/* Create directory for save files */
|
|
if (!fs::exists("storage"))
|
|
{
|
|
try
|
|
{
|
|
fs::create_directory("storage");
|
|
}
|
|
catch (fs::filesystem_error error)
|
|
{
|
|
std::ostringstream message;
|
|
message << "Could not create storage directory. Scores will not be saved. " << error.what();
|
|
sb::Log::log(message, sb::Log::ERR);
|
|
}
|
|
}
|
|
|
|
/* Save scores */
|
|
std::ofstream arcade_scores_file {arcade_scores_file_path};
|
|
if (arcade_scores_file << std::setw(4) << arcade_scores.json(date_format) << std::endl)
|
|
{
|
|
std::ostringstream message;
|
|
message << "Successfully saved arcade scores to " << arcade_scores_file_path;
|
|
sb::Log::log(message);
|
|
}
|
|
else
|
|
{
|
|
std::ostringstream message;
|
|
message << "Could not save arcade scores to " << arcade_scores_file_path;
|
|
sb::Log::log(message, sb::Log::ERR);
|
|
}
|
|
arcade_scores_file.close();
|
|
|
|
#if defined(EMSCRIPTEN)
|
|
EM_ASM(
|
|
FS.syncfs(false, function(error) {
|
|
if (error !== null)
|
|
{
|
|
console.log("Error syncing storage using Filesystem API", error);
|
|
}
|
|
});
|
|
);
|
|
#endif
|
|
}
|
|
|
|
int Cakefoot::length() const
|
|
{
|
|
int length = 0;
|
|
|
|
/* Ignore the title and end levels */
|
|
for (auto curve = curves.begin() + 1; curve != curves.end() - 1; curve++) length += curve->length();
|
|
|
|
return length;
|
|
}
|
|
|
|
int Cakefoot::distance() const
|
|
{
|
|
int distance = 0;
|
|
|
|
if (level_index > 0) {
|
|
|
|
distance += int(character.relative(curve()) * curve().length());
|
|
if (level_index > 1)
|
|
for (auto curve = curves.begin() + 1; curve != curves.begin() + level_index; curve++) {
|
|
distance += curve->length(); } }
|
|
|
|
return distance;
|
|
}
|
|
|
|
float Cakefoot::limit() const
|
|
{
|
|
if (arcade())
|
|
{
|
|
float limit = configuration()("challenge", challenge_index, "time limit");
|
|
|
|
if (level_index > 0)
|
|
{
|
|
const nlohmann::json& levels {configuration()("levels")};
|
|
for (auto level = levels.begin() + 1; level != levels.begin() + level_index + 1; level++) {
|
|
std::string level_addition = "level addition";
|
|
std::string checkpoint_addition = "checkpoint addition";
|
|
if (level >= levels.begin() + configuration()("challenge", challenge_index, "advanced").get<int>())
|
|
{
|
|
level_addition += " advanced";
|
|
checkpoint_addition += " advanced";
|
|
}
|
|
if (level < levels.begin() + level_index)
|
|
{
|
|
limit += configuration()("challenge", challenge_index, level_addition).get<float>();
|
|
if (level->contains("checkpoints"))
|
|
limit += configuration()("challenge", challenge_index, checkpoint_addition).get<float>() * level->at("checkpoints").size();
|
|
}
|
|
else if (level->contains("checkpoints"))
|
|
{
|
|
for (const nlohmann::json& checkpoint : level->at("checkpoints"))
|
|
{
|
|
if (checkpoint.at("position").get<float>() <= character.checkpoint())
|
|
limit += configuration()("challenge", challenge_index, checkpoint_addition).get<float>();
|
|
} } }
|
|
|
|
/* Add bank bonus */
|
|
limit += configuration()("progress", "arcade bank").get<int>() * configuration()(
|
|
"challenge", challenge_index, "bank bonus").get<float>();
|
|
}
|
|
|
|
return limit;
|
|
}
|
|
else return 0.0f;
|
|
}
|
|
|
|
bool Cakefoot::arcade() const
|
|
{
|
|
return configuration()("challenge", challenge_index).contains("time limit");
|
|
}
|
|
|
|
bool Cakefoot::quest() const
|
|
{
|
|
return !arcade() && !level_select();
|
|
}
|
|
|
|
bool Cakefoot::level_select() const
|
|
{
|
|
return !arcade() && configuration()("challenge", challenge_index, "name") == "LEVEL SELECT";
|
|
}
|
|
|
|
bool Cakefoot::end_screen(std::optional<std::size_t> index) const
|
|
{
|
|
if (!index.has_value())
|
|
{
|
|
index = level_index;
|
|
}
|
|
return static_cast<std::size_t>(index.value()) == _configuration("levels").size() - 1;
|
|
}
|
|
|
|
bool Cakefoot::resuming() const
|
|
{
|
|
return configuration()("challenge", challenge_index, "name") == "RESUME QUEST" ||
|
|
configuration()("challenge", challenge_index, "name") == "RESUME ARCADE";
|
|
}
|
|
|
|
std::size_t Cakefoot::bank() const
|
|
{
|
|
std::string mode = quest() ? "quest bank" : "arcade bank";
|
|
return configuration()("progress", mode).get<std::size_t>();
|
|
}
|
|
|
|
std::size_t Cakefoot::max_bank() const
|
|
{
|
|
return configuration()("levels").size() - 2;
|
|
}
|
|
|
|
void Cakefoot::collect_coin(bool add_to_bank)
|
|
{
|
|
if (!coin_collected)
|
|
{
|
|
for (auto& enemy : enemies)
|
|
{
|
|
if (enemy->coin_taken())
|
|
{
|
|
enemy->collect_coin();
|
|
coin_collected = true;
|
|
if (add_to_bank)
|
|
{
|
|
if (arcade())
|
|
{
|
|
configuration()["progress"]["arcade bank"].get_ref<nlohmann::json::number_integer_t&>()++;
|
|
configuration()["progress"]["arcade coin"] = true;
|
|
write_progress();
|
|
}
|
|
else if (quest())
|
|
{
|
|
configuration()["progress"]["quest bank"].get_ref<nlohmann::json::number_integer_t&>()++;
|
|
configuration()["progress"]["quest coin"] = true;
|
|
write_progress();
|
|
} } } } }
|
|
}
|
|
|
|
void Cakefoot::end_game_over_display()
|
|
{
|
|
load_level(configuration()("levels").size() - 1);
|
|
}
|
|
|
|
float Cakefoot::arcade_time_remaining(float limit) const
|
|
{
|
|
return std::max(0.0f, limit - run_timer.elapsed());
|
|
}
|
|
|
|
void Cakefoot::submit_score()
|
|
{
|
|
arcade_score.name = name_entry;
|
|
arcade_scores.add(arcade_score);
|
|
write_scores();
|
|
refresh_scoreboard();
|
|
load_level(0);
|
|
}
|
|
|
|
void Cakefoot::set_arcade_score(float extended_limit, int maximum_distance)
|
|
{
|
|
arcade_score = ArcadeScores::Score {arcade_time_remaining(extended_limit), maximum_distance};
|
|
int rank = std::min(9999, arcade_scores.rank(arcade_score));
|
|
std::ostringstream rank_str, distance_str;
|
|
rank_str << rank;
|
|
if (rank == 1)
|
|
{
|
|
rank_str << "st";
|
|
}
|
|
else if (rank == 2)
|
|
{
|
|
rank_str << "nd";
|
|
}
|
|
else if (rank == 3)
|
|
{
|
|
rank_str << "rd";
|
|
}
|
|
else
|
|
{
|
|
rank_str << "th";
|
|
}
|
|
label.at("arcade rank").content(rank_str.str());
|
|
label.at("arcade rank").refresh();
|
|
distance_str << arcade_score.distance << "m";
|
|
label.at("arcade distance").content(distance_str.str());
|
|
label.at("arcade distance").refresh();
|
|
}
|
|
|
|
void Cakefoot::shift_hue()
|
|
{
|
|
rotating_hue.shift_hue(configuration()("display", "hue shift").get<float>());
|
|
}
|
|
|
|
void Cakefoot::blink()
|
|
{
|
|
blinking_visible = !blinking_visible;
|
|
}
|
|
|
|
void Cakefoot::next_splash()
|
|
{
|
|
if (static_cast<std::size_t>(splash_index) < splash.size() - 1)
|
|
{
|
|
splash_index++;
|
|
splash_animation.play_once(splash[splash_index].length);
|
|
world_color = splash[splash_index].background.normal();
|
|
}
|
|
else
|
|
{
|
|
splash_animation.pause();
|
|
load_level(0);
|
|
}
|
|
}
|
|
|
|
void Cakefoot::flash_warning()
|
|
{
|
|
if (arcade() && run_timer && run_timer.elapsed() + configuration()("display", "arcade warning start").get<float>() > limit())
|
|
{
|
|
arcade_limit_warning = !arcade_limit_warning;
|
|
|
|
/* Depth into the warning range determines the speed of the warning */
|
|
nlohmann::json frequency = configuration()("display", "arcade warning frequency");
|
|
float delay = (run_timer.elapsed() + configuration()("display", "arcade warning start").get<float>() - limit()) /
|
|
configuration()("display", "arcade warning start").get<float>() * (
|
|
frequency[1].get<float>() - frequency[0].get<float>()) + frequency[0].get<float>();
|
|
warning_animation.frame_length(delay);
|
|
}
|
|
else
|
|
{
|
|
arcade_limit_warning = false;
|
|
}
|
|
}
|
|
|
|
std::string Cakefoot::format_clock(float amount)
|
|
{
|
|
int minutes = int(amount) / 60;
|
|
float seconds = amount - (minutes * 60);
|
|
std::stringstream clock;
|
|
clock << std::setw(2) << std::setfill('0') << minutes << ":" << std::setw(4) << std::setprecision(1) << std::fixed << seconds;
|
|
return clock.str();
|
|
}
|
|
|
|
void Cakefoot::refresh_scoreboard()
|
|
{
|
|
std::string text {arcade_scores.formatted(4, 4)};
|
|
scoreboard.content(text);
|
|
scoreboard.refresh();
|
|
}
|
|
|
|
void Cakefoot::respond(SDL_Event& event)
|
|
{
|
|
Game::respond(event);
|
|
|
|
/* Reset the idle timer */
|
|
idle_timer.reset();
|
|
|
|
/* Translate gamepad input to commands */
|
|
if (event.type == SDL_JOYBUTTONDOWN)
|
|
{
|
|
if (level_index > 0 && static_cast<std::size_t>(level_index) <= configuration()("levels").size() - 2 &&
|
|
event.jbutton.button == configuration()("input", "gamepad pause button index"))
|
|
{
|
|
sb::Delegate::post("pause");
|
|
}
|
|
else if (configuration()("demo", "active") && level_index > 0 &&
|
|
static_cast<std::size_t>(level_index) <= configuration()("levels").size() - 2 &&
|
|
event.jbutton.button == configuration()("input", "gamepad reset button index"))
|
|
{
|
|
sb::Delegate::post("reset");
|
|
}
|
|
else if ((!use_play_button || button.at("play").pressed()) && !splash_animation.playing())
|
|
{
|
|
sb::Delegate::post("any");
|
|
}
|
|
}
|
|
else if (event.type == SDL_JOYBUTTONUP)
|
|
{
|
|
sb::Delegate::post("any", true);
|
|
}
|
|
else if (event.type == SDL_JOYAXISMOTION && !cooldown_animation.playing())
|
|
{
|
|
if (event.jaxis.axis == 1)
|
|
{
|
|
if (event.jaxis.value > 15000)
|
|
{
|
|
sb::Delegate::post("down");
|
|
cooldown_animation.play_once(configuration()("input", "gamepad axis cooldown"));
|
|
}
|
|
else if (event.jaxis.value < -15000)
|
|
{
|
|
sb::Delegate::post("up");
|
|
cooldown_animation.play_once(configuration()("input", "gamepad axis cooldown"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (event.jaxis.value > 15000)
|
|
{
|
|
sb::Delegate::post("right");
|
|
cooldown_animation.play_once(configuration()("input", "gamepad axis cooldown"));
|
|
}
|
|
else if (event.jaxis.value < -15000)
|
|
{
|
|
sb::Delegate::post("left");
|
|
cooldown_animation.play_once(configuration()("input", "gamepad axis cooldown"));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Get mouse button states */
|
|
bool left_mouse_pressed = SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON_LMASK;
|
|
bool shift_pressed = SDL_GetModState() & KMOD_SHIFT;
|
|
|
|
/* Get mouse coordinates in pixel resolution and NDC. These values are invalid if the event isn't a mouse event. */
|
|
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;
|
|
|
|
/* Ignore most events when play button or splash screen is active */
|
|
if ((!use_play_button || button.at("play").pressed()) && !splash_animation.playing())
|
|
{
|
|
/* Custom keys for the title screen */
|
|
if (level_index == 0)
|
|
{
|
|
bool challenge_enabled = button.at("challenge decrement").enabled();
|
|
bool level_enabled = button.at("level decrement").enabled();
|
|
bool profile_enabled = button.at("profile decrement").enabled();
|
|
bool view_enabled = button.at("view decrement").enabled();
|
|
|
|
/* Prevent navigating into menus in demo and arcade-only modes */
|
|
if (sb::Delegate::compare(event, "down") && !configuration()("display", "arcade only") &&
|
|
!configuration()("demo", "active"))
|
|
{
|
|
if (selected == "start")
|
|
{
|
|
if (challenge_enabled) selected = "challenge decrement";
|
|
else if (level_enabled) selected = "level decrement";
|
|
else if (profile_enabled) selected = "profile decrement";
|
|
else if (view_enabled) selected = "view decrement";
|
|
}
|
|
else if (selected == "challenge decrement")
|
|
{
|
|
selected = "challenge increment";
|
|
}
|
|
else if (selected == "challenge increment")
|
|
{
|
|
if (level_enabled) selected = "level decrement";
|
|
else if (profile_enabled) selected = "profile decrement";
|
|
else if (view_enabled) selected = "view decrement";
|
|
else selected = "start";
|
|
}
|
|
else if (selected == "level decrement")
|
|
{
|
|
selected = "level increment";
|
|
}
|
|
else if (selected == "level increment")
|
|
{
|
|
if (profile_enabled) selected = "profile decrement";
|
|
else if (view_enabled) selected = "view decrement";
|
|
else selected = "start";
|
|
}
|
|
else if (selected == "profile decrement")
|
|
{
|
|
selected = "profile increment";
|
|
}
|
|
else if (selected == "profile increment")
|
|
{
|
|
if (view_enabled) selected = "view decrement";
|
|
else selected = "start";
|
|
}
|
|
else if (selected == "view decrement")
|
|
{
|
|
selected = "view increment";
|
|
}
|
|
else
|
|
{
|
|
selected = "start";
|
|
}
|
|
}
|
|
|
|
/* Prevent navigating into menus in demo and arcade-only modes */
|
|
else if (sb::Delegate::compare(event, "up") && !configuration()("display", "arcade only") &&
|
|
!configuration()("demo", "active"))
|
|
{
|
|
if (selected == "start")
|
|
{
|
|
if (view_enabled) selected = "view increment";
|
|
else if (profile_enabled) selected = "profile increment";
|
|
else if (level_enabled) selected = "level increment";
|
|
else if (challenge_enabled) selected = "challenge increment";
|
|
}
|
|
else if (selected == "challenge increment")
|
|
{
|
|
selected = "challenge decrement";
|
|
}
|
|
else if (selected == "challenge decrement")
|
|
{
|
|
selected = "start";
|
|
}
|
|
else if (selected == "level increment")
|
|
{
|
|
selected = "level decrement";
|
|
}
|
|
else if (selected == "level decrement")
|
|
{
|
|
if (challenge_enabled) selected = "challenge increment";
|
|
else selected = "start";
|
|
}
|
|
else if (selected == "profile increment")
|
|
{
|
|
selected = "profile decrement";
|
|
}
|
|
else if (selected == "profile decrement")
|
|
{
|
|
if (level_enabled) selected = "level increment";
|
|
else if (challenge_enabled) selected = "challenge increment";
|
|
else selected = "start";
|
|
}
|
|
else if (selected == "view increment")
|
|
{
|
|
selected = "view decrement";
|
|
}
|
|
else if (selected == "view decrement")
|
|
{
|
|
if (profile_enabled) selected = "profile increment";
|
|
else if (level_enabled) selected = "level increment";
|
|
else if (challenge_enabled) selected = "challenge increment";
|
|
else selected = "start";
|
|
}
|
|
else
|
|
{
|
|
selected = "start";
|
|
}
|
|
}
|
|
|
|
/* Execute menu action */
|
|
else if (sb::Delegate::compare(event, "any"))
|
|
{
|
|
if (!selected.has_value())
|
|
{
|
|
button.at("start").press();
|
|
}
|
|
else
|
|
{
|
|
button.at(selected.value()).press();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Custom keys for name entry. */
|
|
else if (static_cast<std::size_t>(level_index) == configuration()("levels").size() - 1 && arcade() &&
|
|
configuration()("display", "name entry enabled"))
|
|
{
|
|
if (sb::Delegate::compare(event, "up"))
|
|
{
|
|
button.at("name " + std::to_string(name_entry_index + 1) + " increment").press();
|
|
}
|
|
else if (sb::Delegate::compare(event, "right"))
|
|
{
|
|
if (++name_entry_index > 2) name_entry_index = 0;
|
|
}
|
|
else if (sb::Delegate::compare(event, "down"))
|
|
{
|
|
button.at("name " + std::to_string(name_entry_index + 1) + " decrement").press();
|
|
}
|
|
else if (sb::Delegate::compare(event, "left"))
|
|
{
|
|
if (--name_entry_index < 0) name_entry_index = 2;
|
|
}
|
|
}
|
|
|
|
/* Custom keys for pause menu */
|
|
else if (!unpaused_timer)
|
|
{
|
|
if (sb::Delegate::compare(event, "up") || sb::Delegate::compare(event, "down"))
|
|
{
|
|
if (selected == "resume")
|
|
{
|
|
selected = "reset";
|
|
}
|
|
else
|
|
{
|
|
selected = "resume";
|
|
}
|
|
}
|
|
else if (selected.has_value() && sb::Delegate::compare(event, "any"))
|
|
{
|
|
button.at(selected.value()).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"))
|
|
{
|
|
/* 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",
|
|
"challenge increment", "challenge decrement", "view increment", "view decrement"})
|
|
{
|
|
if (!configuration()("display", "arcade only") || name == "start")
|
|
{
|
|
if (button.at(name).enabled() && 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;
|
|
}
|
|
|
|
/* 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;
|
|
} } }
|
|
|
|
/* Collide with name entry in arcade mode on end screen */
|
|
else if (static_cast<std::size_t>(level_index) == configuration()("levels").size() - 1 &&
|
|
arcade() && configuration()("display", "name entry enabled"))
|
|
{
|
|
for (const std::string& button_name : {std::string("name 1"), std::string("name 2"), std::string("name 3"),
|
|
"name " + std::to_string(name_entry_index + 1) + " increment",
|
|
"name " + std::to_string(name_entry_index + 1) + " decrement"})
|
|
{
|
|
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;
|
|
} } }
|
|
|
|
/* Collide with social buttons */
|
|
else if (level_index == 0 && configuration()("display", "social media click"))
|
|
{
|
|
for (const std::string& button_name : {"diskmem", "azuria sky"})
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
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();
|
|
arcade_limit_warning = false;
|
|
|
|
/* In arcade-only mode, reset the level select to the first level */
|
|
if (configuration()("display", "arcade only"))
|
|
{
|
|
level_select_index = 1;
|
|
set_up_buttons();
|
|
}
|
|
}
|
|
|
|
else if (sb::Delegate::compare(event, "pause") && level_index > 0 && static_cast<std::size_t>(level_index) <= configuration()("levels").size() - 2)
|
|
{
|
|
if (!unpaused_timer)
|
|
{
|
|
sb::Delegate::post("resume", false);
|
|
}
|
|
else
|
|
{
|
|
/* Pause */
|
|
unpaused_timer.off();
|
|
run_timer.off();
|
|
|
|
/* 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();
|
|
}
|
|
}
|
|
}
|
|
|
|
else if (sb::Delegate::compare(event, "resume") && !unpaused_timer &&
|
|
level_index > 0 && static_cast<std::size_t>(level_index) <= configuration()("levels").size() - 2)
|
|
{
|
|
selected.reset();
|
|
|
|
/* Unpause */
|
|
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();
|
|
}
|
|
}
|
|
|
|
else if (sb::Delegate::compare(event, "pause for ads"))
|
|
{
|
|
/* Store current volume to be restored when returning from ads by looking at the state of the button. */
|
|
if (button.at("volume").pressed())
|
|
{
|
|
pre_ad_volume = 1.0f;
|
|
}
|
|
else
|
|
{
|
|
pre_ad_volume = 0.0f;
|
|
}
|
|
std::ostringstream message;
|
|
message << "Pre-ad volume registered as " << pre_ad_volume.value();
|
|
sb::Log::log(message);
|
|
|
|
/* Mute without changing the state of the button to avoid losing the original state if this event is fired twice in a row. */
|
|
Mix_Volume(-1, 0);
|
|
|
|
if (level_index > 0 && static_cast<std::size_t>(level_index) <= configuration()("levels").size() - 2)
|
|
{
|
|
/* Pause game */
|
|
unpaused_timer.off();
|
|
run_timer.off();
|
|
}
|
|
}
|
|
|
|
else if (sb::Delegate::compare(event, "unpause for ads"))
|
|
{
|
|
/* Restore volume to the volume of the mixer before the ads started */
|
|
std::ostringstream message;
|
|
if (pre_ad_volume.has_value())
|
|
{
|
|
Mix_Volume(-1, sb::audio::convert_volume(pre_ad_volume.value()));
|
|
message << "Restoring volume to " << pre_ad_volume.value();
|
|
}
|
|
else
|
|
{
|
|
message << "Not restoring volume because pre-ad value was not registered";
|
|
}
|
|
sb::Log::log(message);
|
|
|
|
if (level_index > 0 && static_cast<std::size_t>(level_index) <= configuration()("levels").size() - 2)
|
|
{
|
|
/* Unpause game */
|
|
unpaused_timer.on();
|
|
run_timer.on();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
#if !defined(__MINGW32__) && !defined(__MACOS__)
|
|
/* 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) && !defined(__UBUNTU18__)
|
|
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;
|
|
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 / 1'000'000.0 << " MB";
|
|
if ((++count % 2) == 0)
|
|
{
|
|
message << std::endl;
|
|
}
|
|
}
|
|
message << "---" << std::endl;
|
|
sb::Log::log(message);
|
|
}
|
|
#endif
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
else if (use_play_button && !button.at("play").pressed())
|
|
{
|
|
/* Collide with play button */
|
|
if ((event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEMOTION) && button.at("play").collide(mouse_ndc, view, projection))
|
|
{
|
|
if (event.type == SDL_MOUSEBUTTONDOWN)
|
|
{
|
|
button.at("play").press();
|
|
}
|
|
else
|
|
{
|
|
hovering = true;
|
|
}
|
|
}
|
|
|
|
/* Any keyboard input causes the play button to press */
|
|
else if (sb::Delegate::compare(event, "any"))
|
|
{
|
|
button.at("play").press();
|
|
}
|
|
}
|
|
|
|
/* Always collide with volume button and fullscreen if enabled */
|
|
for (const std::string& name : {"volume", "fullscreen"})
|
|
{
|
|
if (name != "fullscreen" || configuration()("display", "fullscreen enabled"))
|
|
{
|
|
if ((event.type == SDL_MOUSEBUTTONDOWN ||
|
|
event.type == SDL_MOUSEMOTION) && button.at(name).collide(mouse_ndc, view, projection))
|
|
{
|
|
if (event.type == SDL_MOUSEBUTTONDOWN)
|
|
{
|
|
button.at(name).press();
|
|
}
|
|
else
|
|
{
|
|
hovering = true;
|
|
}
|
|
break;
|
|
} } }
|
|
|
|
/* 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());
|
|
}
|
|
}
|
|
|
|
bool Cakefoot::paused() const
|
|
{
|
|
return !unpaused_timer;
|
|
}
|
|
|
|
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(levels_file_path);
|
|
#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());
|
|
|
|
/* Keep animation time updated */
|
|
game_over_animation.update(timestamp);
|
|
submit_score_animation.update(timestamp);
|
|
shift_hue_animation.update(timestamp);
|
|
flash_animation.update(timestamp);
|
|
blink_animation.update(timestamp);
|
|
cooldown_animation.update(timestamp);
|
|
splash_animation.update(timestamp);
|
|
warning_animation.update(timestamp);
|
|
|
|
/* 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});
|
|
|
|
/* 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);
|
|
|
|
/* Rotate X 180 if mirror mode is active */
|
|
float rotation_x = rotation.x;
|
|
float rotation_y = rotation.y;
|
|
if (view_index == 1)
|
|
{
|
|
rotation_x += glm::pi<float>();
|
|
}
|
|
else if (view_index == 2)
|
|
{
|
|
glm::vec2 warp = configuration()("levels", level_index, "warp");
|
|
rotation_x += warp.x;
|
|
rotation_y += warp.y;
|
|
}
|
|
|
|
/* Transformation that applies the rotation state of the entire scene */
|
|
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});
|
|
|
|
/* Clear screen to world color */
|
|
if (flash_animation.playing())
|
|
{
|
|
sb::Color extra_shift = rotating_hue;
|
|
extra_shift.shift_hue(180.0f);
|
|
glm::vec4 clear = extra_shift.normal() / configuration()("display", "flash darken factor").get<float>() + world_color;
|
|
glClearColor(clear.r, clear.g, clear.b, clear.a);
|
|
}
|
|
else if (arcade_limit_warning)
|
|
{
|
|
glm::vec4 clear = world_color + configuration()("display", "arcade warning color").get<glm::vec4>();
|
|
glClearColor(clear.r, clear.g, clear.b, clear.a);
|
|
}
|
|
else
|
|
{
|
|
glClearColor(world_color.r, world_color.g, world_color.b, world_color.a);
|
|
}
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
|
|
/* Ignore most of the update and draw loop if the play button is enabled and hasn't been pressed */
|
|
if (!use_play_button || button.at("play").pressed())
|
|
{
|
|
/* Continue ignoring while displaying the splash screen */
|
|
if (!splash_animation.playing())
|
|
{
|
|
/* Update other timers */
|
|
run_timer.update(timestamp);
|
|
configuration()["progress"]["total time"].get_ref<nlohmann::json::number_float_t&>() += run_timer.frame();
|
|
if (arcade())
|
|
{
|
|
configuration()["progress"]["arcade time"].get_ref<nlohmann::json::number_float_t&>() += run_timer.frame();
|
|
}
|
|
else if (quest())
|
|
{
|
|
configuration()["progress"]["quest time"].get_ref<nlohmann::json::number_float_t&>() += run_timer.frame();
|
|
}
|
|
unpaused_timer.update(timestamp);
|
|
idle_timer.update(timestamp);
|
|
|
|
/* In demo mode, reset game if idle timeout elapsed, or reset idle timer if character is accelerating */
|
|
if (level_index > 0 && configuration()("demo", "active") && idle_timer.elapsed() > configuration()("demo", "idle timeout"))
|
|
{
|
|
sb::Delegate::post("reset");
|
|
}
|
|
else if (character.accelerating)
|
|
{
|
|
idle_timer.reset();
|
|
}
|
|
|
|
/* Arcade scoring */
|
|
auto& maximum_distance = configuration()["progress"]["arcade max distance"].get_ref<nlohmann::json::number_integer_t&>();
|
|
float extended_limit = limit();
|
|
if (arcade())
|
|
{
|
|
/* Check if maximum distance increased. Using auto as the type handles differences between integer types in
|
|
* different compilers. */
|
|
if (distance() > maximum_distance)
|
|
{
|
|
maximum_distance = distance();
|
|
}
|
|
|
|
/* End run if there is a time limit and the time limit is passed. Queue end level to load after a delay. */
|
|
bool game_over_active = arcade() && level_index > 0 &&
|
|
run_timer.elapsed() > extended_limit &&
|
|
static_cast<std::size_t>(level_index) < configuration()("levels").size() - 1;
|
|
if (game_over_active && !game_over_animation.playing())
|
|
{
|
|
run_timer.off();
|
|
arcade_limit_warning = false;
|
|
|
|
/* Play once with a delay to let the game over screen display temporarily before the end level is loaded. */
|
|
game_over_animation.play_once(configuration()("display", "game over display time"));
|
|
|
|
/* Create arcade score */
|
|
set_arcade_score(extended_limit, maximum_distance);
|
|
}
|
|
}
|
|
|
|
/* Freeze screen while game over display is active. */
|
|
if (!game_over_animation.playing())
|
|
{
|
|
/* 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(),
|
|
level_index ? std::nullopt : std::optional<float>(configuration()("character", "idle speed").get<float>()));
|
|
if (character.at_end(curve()))
|
|
{
|
|
/* On the ending screen, submit the score and name entry. */
|
|
if (arcade() && static_cast<std::size_t>(level_index) == configuration()("levels").size() - 1)
|
|
{
|
|
submit_score();
|
|
}
|
|
else
|
|
{
|
|
/* On the final arcade level, create a score since game is over */
|
|
if (arcade() && static_cast<std::size_t>(level_index) == configuration()("levels").size() - 2)
|
|
{
|
|
set_arcade_score(extended_limit, maximum_distance);
|
|
}
|
|
|
|
/* Collect any previously taken coins */
|
|
collect_coin();
|
|
|
|
/* Load next level, or reload current level if in level select mode or on title screen */
|
|
audio.at("teleport").play();
|
|
load_level(level_select() || level_index == 0 ? level_index : level_index + 1);
|
|
|
|
#if defined(__COOLMATH__)
|
|
/* Trigger an ad when a level is beaten */
|
|
if (level_index > 0 && static_cast<std::size_t>(level_index) < configuration()("levels").size() - 1)
|
|
{
|
|
EM_ASM(cmgAdBreak());
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
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 */
|
|
collect_coin();
|
|
|
|
/* Save progress */
|
|
if (arcade())
|
|
{
|
|
configuration()["progress"]["arcade checkpoint"] = character.checkpoint();
|
|
}
|
|
else if (quest())
|
|
{
|
|
configuration()["progress"]["quest checkpoint"] = character.checkpoint();
|
|
}
|
|
write_progress();
|
|
} } }
|
|
|
|
/* 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();
|
|
} }
|
|
|
|
/* Collide with ending screen coins */
|
|
if (end_screen())
|
|
{
|
|
ending_coins.erase(
|
|
std::remove_if(
|
|
ending_coins.begin(),
|
|
ending_coins.end(),
|
|
[&](Flame& coin)
|
|
{
|
|
if (coin.collide(character.box(), character.sprite(), {-1.0f, -1.0f, -1.0f}, {1.0f, 1.0f, 1.0f}))
|
|
{
|
|
coin.camo() ? audio.at("take").play() : audio.at("bong").play();
|
|
return true;
|
|
}
|
|
return false;
|
|
}),
|
|
ending_coins.end());
|
|
}
|
|
|
|
/* Respawn */
|
|
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&>()++;
|
|
write_progress();
|
|
} } }
|
|
|
|
/* Plane position vertices will be used for everything before the curve */
|
|
sb::Plane::position->bind("vertex_position", shader_program);
|
|
|
|
/* Disable texture */
|
|
glUniform1i(uniform["texture enabled"], false);
|
|
|
|
/* Reset color addition, and draw curve. */
|
|
if (flash_animation.playing())
|
|
{
|
|
glUniform4fv(uniform.at("color addition"), 1, &rotating_hue.normal()[0]);
|
|
}
|
|
else
|
|
{
|
|
glUniform4fv(uniform.at("color addition"), 1, &glm::vec4(0)[0]);
|
|
}
|
|
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.at("mvp"), view * rotation_matrix, projection, uniform.at("texture enabled"), rotating_hue,
|
|
uniform.at("color addition"));
|
|
if (!flash_animation.playing())
|
|
{
|
|
glUniform4fv(uniform.at("color addition"), 1, &glm::vec4(0)[0]);
|
|
}
|
|
}
|
|
|
|
/* Draw cake */
|
|
character.draw(curve(), uniform.at("mvp"), view * rotation_matrix, projection, uniform.at("texture enabled"));
|
|
|
|
/* Draw end screen coins */
|
|
if (end_screen())
|
|
{
|
|
for (Flame& coin : ending_coins)
|
|
{
|
|
coin.draw(uniform.at("mvp"), view * rotation_matrix, projection, uniform.at("texture enabled"), rotating_hue,
|
|
uniform.at("color addition"));
|
|
if (!flash_animation.playing())
|
|
{
|
|
glUniform4fv(uniform.at("color addition"), 1, &glm::vec4(0)[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Check if any buttons should be disabled */
|
|
bool profile_spinner_enabled = configuration()("progress", "max difficulty") > 0 && !resuming();
|
|
bool profile_spinner_visible = configuration()("progress", "max difficulty") > 0;
|
|
bool view_spinner_enabled = configuration()("progress", "max view") > 0;
|
|
button.at("level decrement").visible(level_select());
|
|
button.at("level decrement").enabled(level_select());
|
|
button.at("level increment").visible(level_select());
|
|
button.at("level increment").enabled(level_select());
|
|
button.at("profile decrement").visible(profile_spinner_enabled);
|
|
button.at("profile decrement").enabled(profile_spinner_enabled);
|
|
button.at("profile increment").visible(profile_spinner_enabled);
|
|
button.at("profile increment").enabled(profile_spinner_enabled);
|
|
button.at("view decrement").enabled(view_spinner_enabled);
|
|
button.at("view decrement").visible(view_spinner_enabled);
|
|
button.at("view increment").enabled(view_spinner_enabled);
|
|
button.at("view increment").visible(view_spinner_enabled);
|
|
|
|
/* Draw buttons. Don't include rotation matrix in view, so buttons will remain flat in the z-dimension. */
|
|
glm::mat4 label_transformation {0.0f};
|
|
if (level_index == 0)
|
|
{
|
|
/* Flash play button */
|
|
if (!configuration()("display", "use arcade prompt"))
|
|
{
|
|
glUniform4fv(uniform.at("color addition"), 1, &rotating_hue.normal()[0]);
|
|
}
|
|
if (selected != "start" || blinking_visible)
|
|
{
|
|
button.at("start").draw(uniform["mvp"], view, projection, uniform["texture enabled"]);
|
|
}
|
|
if (!flash_animation.playing())
|
|
{
|
|
glUniform4fv(uniform.at("color addition"), 1, &glm::vec4(0)[0]);
|
|
}
|
|
|
|
/* Disable spinners if arcade prompt displayed */
|
|
if (!configuration()("display", "use arcade prompt"))
|
|
{
|
|
/* Draw spinner buttons */
|
|
for (const std::string& name : {
|
|
"level decrement", "level increment", "profile decrement", "profile increment", "challenge decrement",
|
|
"challenge increment", "view decrement", "view increment"
|
|
})
|
|
{
|
|
if (selected != name || blinking_visible)
|
|
{
|
|
button.at(name).draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
}
|
|
}
|
|
|
|
/* Draw spinner labels */
|
|
for (const std::string& name : {"level select", "profile", "challenge", "view"})
|
|
{
|
|
if ((name != "profile" || profile_spinner_visible) && (name != "view" || view_spinner_enabled))
|
|
{
|
|
label.at(name).texture(0).bind();
|
|
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)
|
|
{
|
|
if (!configuration()("display", "arcade only"))
|
|
{
|
|
button.at("pause").draw(uniform["mvp"], view, projection, uniform["texture enabled"]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (std::string name : {"resume", "reset"})
|
|
{
|
|
if (selected != name || blinking_visible)
|
|
{
|
|
button.at(name).draw(uniform["mvp"], view, projection, uniform["texture enabled"]);
|
|
}
|
|
}
|
|
|
|
/* Draw playtester thanks */
|
|
thanks.texture(0).bind();
|
|
label_transformation = projection * view * thanks.transformation();
|
|
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_transformation[0][0]);
|
|
thanks.enable();
|
|
glDrawArrays(GL_TRIANGLES, 0, thanks.attributes("position")->count());
|
|
}
|
|
|
|
/* Draw name entry */
|
|
if (static_cast<std::size_t>(level_index) == configuration()("levels").size() - 1 && arcade())
|
|
{
|
|
for (const std::string& button_name : {std::string("name 1"), std::string("name 2"), std::string("name 3"),
|
|
"name " + std::to_string(name_entry_index + 1) + " increment",
|
|
"name " + std::to_string(name_entry_index + 1) + " decrement"})
|
|
{
|
|
button.at(button_name).draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
} } }
|
|
|
|
/* Draw the clock */
|
|
float amount;
|
|
if (arcade())
|
|
{
|
|
if (static_cast<std::size_t>(level_index) == configuration()("levels").size() - 1)
|
|
{
|
|
amount = arcade_time_remaining(arcade_score.time);
|
|
}
|
|
else
|
|
{
|
|
amount = arcade_time_remaining(extended_limit);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
amount = run_timer.elapsed();
|
|
}
|
|
label.at("clock").content(format_clock(amount));
|
|
label.at("clock").refresh();
|
|
sb::Plane::position->bind("vertex_position", shader_program);
|
|
glUniform1i(uniform["texture enabled"], true);
|
|
label.at("clock").texture(0).bind();
|
|
label_transformation = projection * view * label.at("clock").transformation();
|
|
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_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").refresh();
|
|
label.at("level").texture(0).bind();
|
|
label_transformation = projection * view * label.at("level").transformation();
|
|
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_transformation[0][0]);
|
|
label.at("level").enable();
|
|
glDrawArrays(GL_TRIANGLES, 0, label.at("level").attributes("position")->count());
|
|
}
|
|
|
|
/* Draw game over text */
|
|
if (game_over_animation.playing())
|
|
{
|
|
label.at("game over").texture(0).bind();
|
|
label_transformation = projection * view * label.at("game over").transformation();
|
|
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_transformation[0][0]);
|
|
label.at("game over").enable();
|
|
glDrawArrays(GL_TRIANGLES, 0, label.at("game over").attributes("position")->count());
|
|
}
|
|
|
|
/* Draw idle warning */
|
|
if (level_index > 0 && configuration()("demo", "active") &&
|
|
idle_timer.elapsed() > configuration()("demo", "countdown display timeout"))
|
|
{
|
|
std::stringstream idle_warning_message;
|
|
int remaining = std::ceil(configuration()("demo", "idle timeout").get<float>() - idle_timer.elapsed());
|
|
idle_warning_message << configuration()("demo", "countdown message").get<std::string>() << remaining;
|
|
label.at("idle warning").content(idle_warning_message.str());
|
|
label.at("idle warning").refresh();
|
|
label.at("idle warning").texture(0).bind();
|
|
label_transformation = projection * view * label.at("idle warning").transformation();
|
|
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_transformation[0][0]);
|
|
label.at("idle warning").enable();
|
|
glDrawArrays(GL_TRIANGLES, 0, label.at("idle warning").attributes("position")->count());
|
|
}
|
|
|
|
/* Draw demo message */
|
|
if (level_index == 0 && configuration()("demo", "active"))
|
|
{
|
|
demo_message.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
}
|
|
|
|
/* Draw arcade results */
|
|
if (static_cast<std::size_t>(level_index) == configuration()("levels").size() - 1 && arcade())
|
|
{
|
|
for (const std::string& name : {"arcade rank", "arcade distance"})
|
|
{
|
|
label.at(name).texture(0).bind();
|
|
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());
|
|
} }
|
|
|
|
/* Draw scoreboard, QR, quest best, social, and auto save icon on title screen */
|
|
if (level_index == 0)
|
|
{
|
|
/* Only draw scoreboard if arcade mode is selected */
|
|
if (arcade())
|
|
{
|
|
scoreboard.texture(0).bind();
|
|
label_transformation = projection * view * scoreboard.transformation();
|
|
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_transformation[0][0]);
|
|
scoreboard.enable();
|
|
glDrawArrays(GL_TRIANGLES, 0, scoreboard.attributes("position")->count());
|
|
}
|
|
else if (quest() && configuration()("progress", "quest best") > 0.0f)
|
|
{
|
|
label.at("quest best").texture(0).bind();
|
|
label_transformation = projection * view * label.at("quest best").transformation();
|
|
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_transformation[0][0]);
|
|
label.at("quest best").enable();
|
|
glDrawArrays(GL_TRIANGLES, 0, label.at("quest best").attributes("position")->count());
|
|
}
|
|
|
|
/* Draw QR and social. Only draw auto save if QR is not displayed. */
|
|
if (configuration()("display", "qr display"))
|
|
{
|
|
if (configuration()("display", "qr background display"))
|
|
{
|
|
qr_code_bg.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
}
|
|
qr_code.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
}
|
|
else
|
|
{
|
|
auto_save.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
if (configuration()("display", "social media click"))
|
|
{
|
|
button.at("diskmem").draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
button.at("azuria sky").draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
}
|
|
}
|
|
if (!configuration()("display", "social media click"))
|
|
{
|
|
social.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
}
|
|
}
|
|
|
|
/* Draw end screen messages */
|
|
if (static_cast<std::size_t>(level_index) == configuration()("levels").size() - 1 &&
|
|
!configuration()("display", "arcade only"))
|
|
{
|
|
float y = configuration()("ending", "messages y").get<float>();
|
|
for (std::size_t message_ii = 0; message_ii < ending_messages.size(); message_ii++)
|
|
{
|
|
sb::Text& message = ending_messages[message_ii];
|
|
message.untransform();
|
|
message.translate({0.0f, y, 0.0f});
|
|
message.scale(configuration()("ending", "messages scale"));
|
|
message.texture(0).bind();
|
|
label_transformation = projection * view * message.transformation();
|
|
glUniformMatrix4fv(uniform["mvp"], 1, GL_FALSE, &label_transformation[0][0]);
|
|
message.enable();
|
|
glDrawArrays(GL_TRIANGLES, 0, message.attributes("position")->count());
|
|
if (message_ii == 0)
|
|
{
|
|
y += configuration()("ending", "messages margin").get<float>();
|
|
}
|
|
y += configuration()("ending", "messages step").get<float>();
|
|
} }
|
|
|
|
/* 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);
|
|
label.at("fps").refresh();
|
|
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());
|
|
} } }
|
|
else
|
|
{
|
|
/* Draw splash screen */
|
|
sb::Plane::color->bind("vertex_color", shader_program);
|
|
sb::Plane::position->bind("vertex_position", shader_program);
|
|
splash[splash_index].sprite.bind();
|
|
splash[splash_index].sprite.draw(uniform.at("mvp"), view, projection, uniform.at("texture enabled"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Draw the play button if it is enabled and hasn't been pressed yet */
|
|
sb::Plane::position->bind("vertex_position", shader_program);
|
|
sb::Plane::color->bind("vertex_color", shader_program);
|
|
button.at("play").draw(uniform["mvp"], view, projection, uniform["texture enabled"]);
|
|
}
|
|
|
|
/* Always draw the volume and fullscreen buttons if enabled unless it's arcade only mode */
|
|
if (!configuration()("display", "arcade only"))
|
|
{
|
|
for (const std::string& name : {"volume", "fullscreen"})
|
|
{
|
|
if (name != "fullscreen" || configuration()("display", "fullscreen enabled"))
|
|
{
|
|
sb::Plane::position->bind("vertex_position", shader_program);
|
|
sb::Plane::color->bind("vertex_color", shader_program);
|
|
button.at(name).draw(uniform["mvp"], view, projection, uniform["texture enabled"]);
|
|
} } }
|
|
|
|
/* Update display */
|
|
SDL_GL_SwapWindow(window());
|
|
sb::Log::gl_errors("at end of update");
|
|
}
|
|
|
|
void Cakefoot::quit()
|
|
{
|
|
controller.reset();
|
|
super::quit();
|
|
}
|
|
|
|
#if defined(EMSCRIPTEN)
|
|
EM_BOOL respond_to_visibility_change(int event_type, const EmscriptenVisibilityChangeEvent* visibility_change_event, void* user_data)
|
|
{
|
|
Cakefoot* game = reinterpret_cast<Cakefoot*>(user_data);
|
|
if (visibility_change_event->hidden && !game->paused())
|
|
{
|
|
sb::Delegate::post("pause", false);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
EM_BOOL respond_to_gamepad_connected(int event_type, const EmscriptenGamepadEvent* gamepad_event, void* user_data)
|
|
{
|
|
Cakefoot* game = reinterpret_cast<Cakefoot*>(user_data);
|
|
game->open_game_controller();
|
|
return true;
|
|
}
|
|
|
|
extern "C"
|
|
{
|
|
void pause_for_ads()
|
|
{
|
|
sb::Delegate::post("pause for ads", false);
|
|
}
|
|
|
|
void unpause_for_ads()
|
|
{
|
|
sb::Delegate::post("unpause for ads", false);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
int main()
|
|
{
|
|
#if defined(__ARCADE_ONLY__)
|
|
/* Merge the arcade-only configuration */
|
|
Cakefoot game = Cakefoot({"src/config_arcade.json"});
|
|
#elif defined(__DEMO__)
|
|
/* Merge the demo configuration */
|
|
Cakefoot game = Cakefoot({"src/config_demo.json"});
|
|
#elif defined(__COOLMATH__)
|
|
/* Merge config specific to the coolmath WASM build */
|
|
Cakefoot game = Cakefoot({"src/config_coolmath.json"});
|
|
#else
|
|
Cakefoot game = Cakefoot();
|
|
#endif
|
|
game.run();
|
|
game.quit();
|
|
return 0;
|
|
}
|