612 lines
21 KiB
C++
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();
|