cakefoot/src/Cakefoot.hpp

612 lines
21 KiB
C++

/* _ _
* c/a`k-e'f`o^o~t-, | a single-button action game | by @dankd0tgame
* / _< | wow a living cake the sweet | play online: https://cakefoot.dank.game
* _> `~_/ | taste of victory | open source: https://open.shampoo.ooo/shampoo/cakefoot
*/
#pragma once
/* Needed for functions in glm/gtx/ */
#define GLM_ENABLE_EXPERIMENTAL
/* Standard library includes */
#include <stdlib.h>
#include <string>
#include <iostream>
#include <iomanip>
#include <map>
#include <memory>
#include <functional>
#if !defined(__MACOS__)
#include <malloc.h>
#endif
#include <chrono>
#include <ctime>
/* Include Game.hpp before any other SDL-related headers because it defines SDL_MAIN_HANDLED */
#include "Game.hpp"
/* SPACEBOX external libraries included in source package */
#include "sdl2-gfx/SDL2_gfxPrimitives.h"
#include "json/json.hpp"
#include "glm/glm.hpp"
#include "glm/gtx/matrix_decompose.hpp"
#include "glm/gtc/matrix_access.hpp"
/* SPACEBOX classes and functions */
#include "Color.hpp"
#include "extension.hpp"
#include "filesystem.hpp"
#include "Animation.hpp"
#include "Texture.hpp"
#include "GLObject.hpp"
#include "Log.hpp"
#include "Attributes.hpp"
#include "VBO.hpp"
#include "Model.hpp"
#include "Box.hpp"
#include "Switch.hpp"
#include "Selection.hpp"
#include "math.hpp"
#include "Configuration.hpp"
#include "Timer.hpp"
#include "Text.hpp"
#include "Segment.hpp"
#include "Input.hpp"
/* Project classes */
#include "Character.hpp"
#include "Sprite.hpp"
#include "Pad.hpp"
#include "Curve.hpp"
#include "Enemy.hpp"
/*!
* Container for a list of arcade scores that keeps scores sorted as they are added.
*/
class ArcadeScores
{
public:
struct Score
{
float time = 0.0f;
int distance = 0;
std::string name = "";
std::time_t date;
Score(float time, int distance, const std::string& name = "") : time(time), distance(distance), name(name), date(std::time(nullptr)) {};
Score() : Score(0.0f, 0) {};
bool operator>(const Score& other) const
{
return time > other.time || (time == other.time && distance > other.distance);
}
};
private:
std::vector<Score> scores;
public:
void add(const Score& incoming)
{
auto score = scores.begin();
for (; score != scores.end(); score++) if (incoming > *score) break;
scores.insert(score, incoming);
}
int rank(const Score& other) const
{
std::size_t index = 0;
for (; index < scores.size() && scores[index] > other; index++);
return index + 1;
}
nlohmann::json json(const std::string& date_format) const
{
nlohmann::json json;
for (const ArcadeScores::Score& score : scores)
{
std::ostringstream date;
date << std::put_time(std::localtime(&score.date), date_format.c_str());
nlohmann::json entry = {
{"name", score.name},
{"time", score.time},
{"distance", score.distance},
{"date", date.str()}
};
json.push_back(entry);
}
return json;
}
std::string formatted(int rows, int cols) const
{
std::size_t index = 0;
std::string name;
std::ostringstream text;
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
std::ostringstream score;
index = cols * col + row;
if (index >= scores.size())
{
name = "---";
score << "";
}
else
{
name = scores[index].name;
if (scores[index].time > 0.0f)
{
score << std::setprecision(1) << std::fixed << scores[index].time;
}
else
{
score << scores[index].distance << "m";
}
}
text << std::setw(2) << index + 1 << ". " << name << " " << std::setw(6) << score.str() << " ";
if (col == cols - 1)
{
text << std::endl;
}
}
}
return text.str();
}
};
/*!
* Image to display at game start up, includes a background color and length in seconds to display.
*/
struct Splash
{
sb::Sprite sprite;
sb::Color background;
float length;
};
/*!
* The main game object. There is currently only support for one of these to exist at a time.
*
* Note: the conversion for pixel distance from the original 864x468 coordinate system to this program's NDC coordinate system is
*
* f(distance) = distance / 486.0 * 2
*
* Note: the conversion for pixel speed values from the original Python version's per-frame, fixed 25fps, 864x468 coordinate system to
* this program's per-second, adjustable framerate, NDC coordinate system is below. This gives the speed in amount of NDC to travel per
* second. For non-pixel values, just multiply by 25.
*
* f(speed) = speed / 486.0 * 25 * 2
*/
class Cakefoot : public sb::Game
{
private:
/* Convention for calling parent class in a consistent way across classes */
typedef sb::Game super;
/* Static members */
const inline static std::string reset_command_name = "reset";
const inline static fs::path progress_file_path = "storage/cakefoot_progress.json";
const inline static fs::path arcade_scores_file_path = "storage/cakefoot_arcade_scores.json";
const inline static fs::path levels_file_path = "resource/levels.json";
const inline static std::string date_format = "%Y/%m/%d %H:%M";
/* Member vars */
std::shared_ptr<SDL_Cursor> poke, grab;
int previous_frames_per_second = 0, curve_index = 0, curve_byte_count = 0, level_index = 0, level_select_index = 1,
profile_index = 0, challenge_index = 0, view_index = 0, name_entry_index = 0, splash_index = 0;
std::map<std::string, GLuint> uniform;
GLuint shader_program;
glm::mat4 view {1.0f}, projection {1.0f};
sb::VAO vao;
sb::VBO vbo;
std::map<std::string, sb::Pad<>> button = {
{"start", sb::Pad<>()},
{"resume", sb::Pad<>()},
{"reset", sb::Pad<>()},
{"level increment", sb::Pad<>()},
{"level decrement", sb::Pad<>()},
{"pause", sb::Pad<>()},
{"profile increment", sb::Pad<>()},
{"profile decrement", sb::Pad<>()},
{"volume", sb::Pad<>()},
{"play", sb::Pad<>()},
{"challenge increment", sb::Pad<>()},
{"challenge decrement", sb::Pad<>()},
{"view increment", sb::Pad<>()},
{"view decrement", sb::Pad<>()},
{"name 1", sb::Pad<>()},
{"name 1 increment", sb::Pad<>()},
{"name 1 decrement", sb::Pad<>()},
{"name 2", sb::Pad<>()},
{"name 2 increment", sb::Pad<>()},
{"name 2 decrement", sb::Pad<>()},
{"name 3", sb::Pad<>()},
{"name 3 increment", sb::Pad<>()},
{"name 3 decrement", sb::Pad<>()},
{"fullscreen", sb::Pad<>()},
{"diskmem", sb::Pad<>()},
{"azuria sky", sb::Pad<>()}
};
std::map<std::string, std::shared_ptr<TTF_Font>> fonts {
{"medium", font(configuration()("font", "medium", "path").get<std::string>(), configuration()("font", "medium", "size"))},
{"small", font(configuration()("font", "small", "path").get<std::string>(), configuration()("font", "small", "size"))},
{"large", font(configuration()("font", "large", "path").get<std::string>(), configuration()("font", "large", "size"))},
{"glyph", font(configuration()("font", "glyph", "path").get<std::string>(), configuration()("font", "glyph", "size"))},
{"glyph large", font(configuration()("font", "glyph large", "path").get<std::string>(),
configuration()("font", "glyph large", "size"))}
};
std::map<std::string, sb::Text> label = {
{"fps", sb::Text(font())},
{"clock", sb::Text(font())},
{"level", sb::Text(font())},
{"level select", sb::Text(font())},
{"profile", sb::Text(font())},
{"challenge", sb::Text(font())},
{"view", sb::Text(font())},
{"game over", sb::Text(font())},
{"arcade rank", sb::Text(fonts.at("large"))},
{"arcade distance", sb::Text(fonts.at("large"))},
{"quest best", sb::Text(fonts.at("glyph"))},
{"idle warning", sb::Text(font())}
};
sb::Sprite playing_field, checkpoint_on, checkpoint_off, qr_code, qr_code_bg, social, auto_save, demo_message,
coin {"resource/coin/coin-0.png", glm::vec2{12.0f / 486.0f}, GL_LINEAR};
sb::Timer on_timer, run_timer, unpaused_timer, idle_timer;
glm::vec3 camera_position {0.0f, 0.0f, 2.0f}, subject_position {0.0f, 0.0f, 0.0f};
float zoom = 0.0f;
glm::vec2 rotation = {0.0f, 0.0f};
std::vector<Curve> curves;
std::vector<std::shared_ptr<Enemy>> enemies;
glm::vec4 world_color {0.2f, 0.2f, 0.2f, 1.0f};
std::map<std::string, sb::audio::Chunk> audio;
Character character {_configuration, audio};
bool use_play_button = false, coin_collected = false, blinking_visible = true, arcade_limit_warning = false;
ArcadeScores arcade_scores;
ArcadeScores::Score arcade_score;
std::string name_entry;
sb::Text scoreboard {fonts.at("large")}, thanks {fonts.at("medium")};
std::vector<Flame> ending_coins;
sb::Color rotating_hue {128, 0, 0, 0};
std::vector<sb::Text> ending_messages;
std::optional<std::string> selected;
std::shared_ptr<SDL_GameController> controller = nullptr;
std::optional<float> pre_ad_volume = std::nullopt;
std::vector<Splash> splash;
/*!
* Load sound effects and music into objects that can be used by the SDL mixer library. Use chunk objects for background music instead
* of music objects so background music tracks can fade into each other.
*/
void load_audio();
/*!
* Open configuration and load curve data into the object.
*/
void load_curves();
/*!
* Compile and attach shaders, store locations of uniforms, initialize some GL properties. This must be done after the GL context
* is created (currently the context is created in the Game constructor, so it will have been created already when this is
* called).
*/
void initialize_gl();
/*!
* Create button objects and assign them to the entries in the button map. This can be re-run to apply changes made in the
* configuration or to refresh label content.
*/
void set_up_buttons();
/*!
* Respond to a change in index in the challenge spinner.
*/
void toggle_challenge();
/*!
* Style the HUD elements based on the configuration settings. This can be re-run to apply changes made in the configuration.
*/
void set_up_hud();
/*!
* Generate, bind, and fill a vertex buffer object with the game's vertex data.
*/
void load_vbo();
/*!
* @return The current level's curve
*/
Curve& curve();
/*!
* @return The current level's curve
*/
const Curve& curve() const;
/*!
* Change the level to the given index. Load enemies, update the curve index.
*
* @param index index of the level to load
*/
void load_level(int index);
/*!
* Save the JSON in the `progress` field of Game::configuration to storage at Cakefoot::progress_file_path.
*
* For PC builds, the folder `storage/` will be created in the current working directory if it doesn't exist already. The user must have
* permission to create folders in the directory. The file `cakefoot_progress.json` will also be created if necessary.
*
* For web builds, Emscripten is used to abstract this function so that writing to a browser's Indexed DB is done automatically. The folder
* `storage/` must have been mounted already using Emscripten's FS module.
*/
void write_progress() const;
/*!
* Use the same procedure as Cakefoot::write_progress() to write arcade scores to a JSON file in `storage/cakefoot_arcade_scores.json`.
*
* @see Cakefoot::write_progress()
*/
void write_scores() const;
/*!
* @return Sum of the lengths of all curves on all regular levels.
*/
int length() const;
/*!
* @return The character's current distance from the beginning of the first level.
*/
int distance() const;
/*!
* @return The time limit of the current run. Combines the challenge mode's initial limit with added time for completed levels and checkpoints.
*/
float limit() const;
/*!
* @return True if arcade is the current mode
*/
bool arcade() const;
/*!
* @return True if quest is the current mode
*/
bool quest() const;
/*!
* @return True if level select is the current mode
*/
bool level_select() const;
/*!
* @param index optional level index
* @return True if level is currently the end screen
*/
bool end_screen(std::optional<std::size_t> index = std::nullopt) const;
/*!
* @return True if resume quest or resume arcade is the current mode
*/
bool resuming() const;
/*!
* @return Count of coins collected
*/
std::size_t bank() const;
/*!
* @return Maximum number of coins that can be collected
*/
std::size_t max_bank() const;
inline bool skip_resume_quest()
{
return configuration()("challenge", challenge_index, "name") == "RESUME QUEST" && configuration()("progress", "quest level") == 1 &&
configuration()("progress", "quest checkpoint") == 0.0f;
}
inline bool skip_resume_arcade()
{
return configuration()("challenge", challenge_index, "name") == "RESUME ARCADE" && configuration()("progress", "arcade level") == 1 &&
configuration()("progress", "arcade checkpoint") == 0.0f;
}
inline bool skip_level_select()
{
return level_select() && configuration()("progress", "max difficulty") < 1 && configuration()("progress", "max level") <= 1;
}
/*!
* Remove coin from the enemy and the level. If quest or arcade mode is active and flag is not set, add the coin to the
* appropriate bank.
*
* @param add_to_bank flag to either add to the bank or not
*/
void collect_coin(bool add_to_bank = true);
/*!
* Move the level index to the end level, causing the game over state to end.
*/
void end_game_over_display();
/* This animation can be used to end the game over state after the time limit is reached. Play once with a delay to let the
* game over screen display temporarily before being ended by this animation. */
Animation game_over_animation {std::bind(&Cakefoot::end_game_over_display, this)};
/*!
* Write score, refresh scoreboard, and load the title screen
*/
void submit_score();
/* Can be used to time out the name entry screen */
Animation submit_score_animation {std::bind(&Cakefoot::submit_score, this)};
/*!
*/
void set_arcade_score(float extended_limit, int maximum_distance);
/*!
* Shift the hue of the global sb::Color object that tracks the hue shift by the given amount in the configuration.
*/
void shift_hue();
/* Shift the hue by the configured amount once per configured amount of seconds */
Animation shift_hue_animation {std::bind(&Cakefoot::shift_hue, this)};
/* Flash the screen */
Animation flash_animation;
/*!
* Toggle visibility of the flag used by the blink animation.
*/
void blink();
/* Toggle visibility flag every interval */
Animation blink_animation {std::bind(&Cakefoot::blink, this), configuration()("display", "blink frequency")};
/* Count a cooldown period for gamepad axes */
Animation cooldown_animation;
/*!
* Display next splash image.
*/
void next_splash();
/* Display splash images in succession until all splash images have been displayed. */
Animation splash_animation {std::bind(&Cakefoot::next_splash, this)};
/*!
* Set arcade time limit warning state at Cakefoot::arcade_limit_warning based on mode, time remaining, and whether the
* blinking frame is on or off.
*/
void flash_warning();
/* Test whether or not the arcade time limit warning should be active. */
Animation warning_animation {std::bind(&Cakefoot::flash_warning, this)};
/*!
* Get the arcade time as the amount of time remaining before the limit is reached.
*
* @param limit Time limit of arcade mode
* @return Amount of time remaining in seconds
*/
float arcade_time_remaining(float limit) const;
/*!
* Convert an amount of seconds to MMM:SS.sss format. The amount of minute digits will be as many as necessary.
*
* @param amount Amount in seconds to convert
* @return String formatted to MMM:SS.sss
*/
static std::string format_clock(float amount);
/*!
* Build a texture displaying the top 25 scores and assign the texture to the scoreboard sprite.
*/
void refresh_scoreboard();
public:
/*!
* Initialize a Cakefoot instance, optionally with extra configuration to merge in after the engine and user configs are loaded.
*
* @param configuration_merge list of file paths with configuration JSON to merge in before the game and engine begins to load
*/
Cakefoot(std::initializer_list<std::string> configuration_merge = {});
/*!
* Log detected joysticks and open the first one that is usable as a game controller
*/
void open_game_controller();
/*!
* Respond to command events
*/
void respond(SDL_Event&);
/*!
* @return true if the game is currently paused
*/
bool paused() const;
/*!
* Start timers, enable auto refresh, and run the super class's run function, which starts the game's update loop.
*
* @see Game::run()
*/
void run();
/*!
* Update parameters and draw the screen.
*
* @param timestamp seconds since the start of the program
*/
void update(float timestamp);
/*!
* Close the controller, then call Game::quit()
*/
void quit();
};
#if defined(EMSCRIPTEN)
/*!
* This event handler should be registered with emscripten_set_visibilitychange_callback so it will be called automatically when the
* browser tab is hidden. A pause event will be posted through sb::Delegate::post(const std::string&, bool) when the browser tab is
* hidden.
*
* @param event_type Emscripten's ID for the visibilitychange event
* @param visibility_change_event Emscripten's event object
* @param user_data The game object passed as a void pointer through Emscripten's API
* @return True to indicate that the event was consumed by the handler
*/
EM_BOOL respond_to_visibility_change(int event_type, const EmscriptenVisibilityChangeEvent* visibility_change_event, void* user_data);
/*!
* This event handler should be registered with emscripten_set_gamepadconnected_callback so it will be called automatically when a
* gamepad is connected. If there isn't a gamepad already registered, the first detected gamepad will be registered as the game's
* controller.
*
* Even if a USB gamepad is connected when the program loads, the web browser may not register it as connected until a button is
* pressed, so this event will need to be triggered before any controller is usable on a web page.
*
* @param event_type Emscripten's ID for the gamepadconnected event
* @param visibility_change_event Emscripten's event object
* @param user_data The game object passed as a void pointer through Emscripten's API
* @return True to indicate that the event was consumed by the handler
*/
EM_BOOL respond_to_gamepad_connected(int event_type, const EmscriptenGamepadEvent* gamepad_event, void* user_data);
extern "C"
{
/*!
* Custom pause event for use with ad APIs that posts a "pause for ads" event using sb::Delegate::post(const std::string&, bool). This function will
* be exported for use in the JavaScript code in* a web build and is not available in other types of builds.
*/
void pause_for_ads();
/*!
* Custom pause event for use with ad APIs that posts a "unpause for ads" event using sb::Delegate::post(const std::string&, bool). This function will
* be exported for use in the JavaScript code in a web build and is not available in other types of builds.
*/
void unpause_for_ads();
}
#endif
/*!
* Create a Cakewalk instance and launch its mainloop.
*
* @return Always returns 0
*/
int main();