diff --git a/CMakeLists.txt b/CMakeLists.txt index 86be9e3..82bd1e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ add_executable(waveforge src/items/water.cpp src/items/oil.cpp src/scenes/duckdeath.cpp + src/scenes/help.cpp src/scenes/level_menu.cpp src/scenes/level_switch.cpp src/scenes/level.cpp diff --git a/assets/manifest.json b/assets/manifest.json index 40caeef..1537fdf 100644 --- a/assets/manifest.json +++ b/assets/manifest.json @@ -435,6 +435,24 @@ "input": "ui/level-link/raw", "description": "Creating level link texture" }, + { + "id": "ui/help/raw", + "type": "image", + "file": "ui/help.png", + "description": "Loading help background image" + }, + { + "id": "ui/help", + "type": "create-texture", + "input": "ui/help/raw", + "description": "Creating help background texture" + }, + { + "id": "ui-config/help", + "type": "json", + "file": "ui/help.json", + "description": "Loading help UI configuration" + }, { "id": "ui-config/level-menu", "type": "json", diff --git a/assets/prototype/howtoplay.aseprite b/assets/prototype/howtoplay.aseprite new file mode 100644 index 0000000..050ae22 Binary files /dev/null and b/assets/prototype/howtoplay.aseprite differ diff --git a/assets/prototype/waveforge.aseprite b/assets/prototype/waveforge.aseprite index 32d5400..7ecd46d 100644 Binary files a/assets/prototype/waveforge.aseprite and b/assets/prototype/waveforge.aseprite differ diff --git a/assets/ui/help.aseprite b/assets/ui/help.aseprite new file mode 100644 index 0000000..2087765 Binary files /dev/null and b/assets/ui/help.aseprite differ diff --git a/assets/ui/help.json b/assets/ui/help.json new file mode 100644 index 0000000..5d3c0f0 --- /dev/null +++ b/assets/ui/help.json @@ -0,0 +1,4 @@ +{ + "width": 256, + "height": 192 +} \ No newline at end of file diff --git a/assets/ui/help.png b/assets/ui/help.png new file mode 100644 index 0000000..dd74f5b Binary files /dev/null and b/assets/ui/help.png differ diff --git a/assets/ui/main-menu.json b/assets/ui/main-menu.json index ba7dd85..db3bf20 100644 --- a/assets/ui/main-menu.json +++ b/assets/ui/main-menu.json @@ -19,53 +19,22 @@ "play": { "x": 72, "y": 80, - "size": 2, - "color": [ - 0, - 0, - 0, - 200 - ], - "active-color": [ - 207, - 158, - 9, - 255 - ] + "size": 2 }, "settings": { "x": 72, "y": 104, - "size": 2, - "color": [ - 0, - 0, - 0, - 200 - ], - "active-color": [ - 207, - 158, - 9, - 255 - ] + "size": 2 }, - "exit": { + "help": { "x": 72, "y": 128, - "size": 2, - "color": [ - 0, - 0, - 0, - 200 - ], - "active-color": [ - 207, - 158, - 9, - 255 - ] + "size": 2 + }, + "exit": { + "x": 72, + "y": 152, + "size": 2 } } } \ No newline at end of file diff --git a/assets/ui/settings.json b/assets/ui/settings.json index eaa56fe..95f54d8 100644 --- a/assets/ui/settings.json +++ b/assets/ui/settings.json @@ -28,18 +28,6 @@ "y": 64, "width": 192, "spacing": 12, - "size": 1, - "color": [ - 0, - 0, - 0, - 255 - ], - "active-color": [ - 255, - 200, - 0, - 255 - ] + "size": 1 } } \ No newline at end of file diff --git a/include/wforge/audio.h b/include/wforge/audio.h index f52f915..baf67e6 100644 --- a/include/wforge/audio.h +++ b/include/wforge/audio.h @@ -33,6 +33,7 @@ class BGMManager { void fadeInCurrent(int duration_ticks, float starting_volume); void step(); void nextMusic(); + void setVolume(float volume); private: float _cur_volume; diff --git a/include/wforge/colorpalette.h b/include/wforge/colorpalette.h index a6f6920..246c901 100644 --- a/include/wforge/colorpalette.h +++ b/include/wforge/colorpalette.h @@ -17,6 +17,11 @@ constexpr sf::Color ui_text_color(std::uint8_t a) { return sf::Color(0, 0, 0, a); } +constexpr sf::Color ui_active_color{250, 200, 46, 255}; +constexpr sf::Color ui_text_bright_color(std::uint8_t a) { + return sf::Color(255, 255, 255, a); +} + // All indexed colors must be here, for dynamic generated textures // Colors in static assets (e.g. PNG files) can be outside this palette constexpr ColorPaletteEntry _colors[] = { diff --git a/include/wforge/save.h b/include/wforge/save.h index 77a3de6..4c8d737 100644 --- a/include/wforge/save.h +++ b/include/wforge/save.h @@ -21,6 +21,7 @@ struct SaveData { void save() const; void resetSettings(); void resetAll(); + bool is_first_launch() const noexcept; SaveData(const SaveData &) = delete; SaveData &operator=(const SaveData &) = delete; diff --git a/include/wforge/scene.h b/include/wforge/scene.h index 521073c..b77124f 100644 --- a/include/wforge/scene.h +++ b/include/wforge/scene.h @@ -82,6 +82,12 @@ class SceneManager { int _scale; }; +struct ButtonDescriptor { + int x; + int y; + int size; +}; + namespace scene { struct LevelPlaying { @@ -96,6 +102,9 @@ struct LevelPlaying { const SceneManager &mgr, sf::RenderTarget &target, int scale ) const; + void pause(SceneManager &mgr) noexcept; + void unpause(SceneManager &mgr) noexcept; + private: void _restartLevel(SceneManager &mgr, bool is_failed = true); @@ -105,6 +114,12 @@ struct LevelPlaying { int _hint_type; int _hint_opacity; PixelFont &font; + bool _paused; + + // Paused Menu + int _paused_menu_current_button_index; + bool _show_help; + sf::Texture *_help_texture; }; struct DuckDeath { @@ -242,17 +257,10 @@ struct MainMenu { const PixelFont &font; sf::Texture *_background_texture; - struct ButtonDescriptor { - int x; - int y; - int size; - sf::Color color; - sf::Color active_color; - }; - int _current_button_index; ButtonDescriptor _play_button; ButtonDescriptor _settings_button; + ButtonDescriptor _help_button; ButtonDescriptor _exit_button; UITextDescriptor _version_text; }; @@ -321,6 +329,23 @@ struct Credits { std::vector> _content; }; +struct Help { + Help(); + + std::array size() const; + void setup(SceneManager &mgr); + void handleEvent(SceneManager &mgr, sf::Event &evt); + void step(SceneManager &mgr); + void render( + const SceneManager &mgr, sf::RenderTarget &target, int scale + ) const; + +private: + sf::Texture *_background_texture; + int _width; + int _height; +}; + } // namespace scene } // namespace wf diff --git a/src/audio.cpp b/src/audio.cpp index 5b160ae..6d8406e 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -120,6 +120,14 @@ void BGMManager::step() { } } +void BGMManager::setVolume(float volume) { { + _cur_volume = volume; + if (_cur_bgm) { + _cur_bgm->setVolume(_cur_volume); + } + } +} + void BGMManager::nextMusic() { if (!_collection) { return; diff --git a/src/main.cpp b/src/main.cpp index 5de2c09..db9d813 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,7 +13,7 @@ #include #include -void entry(const std::string &level_id, int scale_config); +void entry(const std::string &level_id, int scale_config, bool is_first_launch); std::filesystem::path wf::_executable_path; int main(int argc, char **argv) { @@ -64,7 +64,10 @@ int main(int argc, char **argv) { } CPPTRACE_TRY { - entry(program.get("level"), program.get("--scale")); + entry( + program.get("level"), program.get("--scale"), + save.is_first_launch() + ); } CPPTRACE_CATCH(const std::exception &e) { std::cerr << "Unhandled exception: " << e.what() << "\n"; @@ -75,14 +78,25 @@ int main(int argc, char **argv) { return 0; } -void entry(const std::string &level_id, int scale_config) { +void entry( + const std::string &level_id, int scale_config, bool is_first_launch +) { + auto initialScene = [&](const std::string &level_id, bool is_first_launch) { + if (level_id == "-") { + if (is_first_launch) { + return pro::make_proxy(); + } else { + return pro::make_proxy(); + } + } else { + return pro::make_proxy( + level_id + ); + } + }; + wf::SceneManager scene_mgr( - level_id == "-" - ? pro::make_proxy() - : pro::make_proxy( - level_id - ), - scale_config + initialScene(level_id, is_first_launch), scale_config ); auto &window = scene_mgr.window; diff --git a/src/save.cpp b/src/save.cpp index f6e5ecf..ac65837 100644 --- a/src/save.cpp +++ b/src/save.cpp @@ -94,7 +94,7 @@ SaveData &SaveData::instance() noexcept { } } - // Please don't relay on this saving mechanism + // Please don't rely on this saving mechanism // it only works as an extra safeguard against data loss // Please save manually whenever change is made std::atexit([]() { @@ -142,6 +142,10 @@ void SaveData::resetAll() { save(); } +bool SaveData::is_first_launch() const noexcept { + return completed_levels == 0; +} + UserSettings UserSettings::defaultSettings() noexcept { return UserSettings{ .scale = 0, diff --git a/src/scenes/help.cpp b/src/scenes/help.cpp new file mode 100644 index 0000000..8f9c099 --- /dev/null +++ b/src/scenes/help.cpp @@ -0,0 +1,53 @@ +#include "wforge/assets.h" +#include "wforge/scene.h" +#include + +namespace wf::scene { + +Help::Help() { + const auto &json_data = AssetsManager::instance().getAsset( + "ui-config/help" + ); + + _width = json_data.at("width"); + _height = json_data.at("height"); + + _background_texture = &AssetsManager::instance().getAsset( + "ui/help" + ); +} + +std::array Help::size() const { + return {_width, _height}; +} + +void Help::handleEvent(SceneManager &mgr, sf::Event &evt) { + if (auto kb = evt.getIf()) { + switch (kb->code) { + case sf::Keyboard::Key::Escape: + case sf::Keyboard::Key::Enter: + case sf::Keyboard::Key::Space: + UISounds::instance().forward.play(); + mgr.changeScene(pro::make_proxy()); + return; + + default: + break; + } + } +} + +void Help::setup(SceneManager &mgr) {} +void Help::step(SceneManager &mgr) {} + +void Help::render( + const SceneManager &mgr, sf::RenderTarget &target, int scale +) const { + sf::Sprite background_sprite(*_background_texture); + background_sprite.setPosition(sf::Vector2f(0, 0)); + background_sprite.setScale(sf::Vector2f(scale, scale)); + + target.draw(background_sprite); +} + +} // namespace wf::scene \ No newline at end of file diff --git a/src/scenes/level.cpp b/src/scenes/level.cpp index 49dea2e..d1e97ac 100644 --- a/src/scenes/level.cpp +++ b/src/scenes/level.cpp @@ -13,6 +13,14 @@ namespace wf::scene { namespace { +enum PausedMenuButton { + RESUME = 0, + RETRY, + HELP, + QUIT, + BUTTON_COUNT +}; + auto loadFont() { static PixelFont *font = nullptr; if (!font) { @@ -56,11 +64,16 @@ LevelPlaying::LevelPlaying(const std::string &level_id) LevelPlaying::LevelPlaying(Level level) : _tick(0) + , _paused(false) + , _show_help(false) + , _paused_menu_current_button_index(PausedMenuButton::RESUME) , _level(std::move(level)) , _renderer(_level) , _hint_type(HintType::None) , _hint_opacity(0) - , font(*loadFont()) {} + , font(*loadFont()) { + _help_texture = &AssetsManager::instance().getAsset("ui/help"); +} std::array LevelPlaying::size() const { return {_level.width(), _level.height()}; @@ -75,7 +88,93 @@ void LevelPlaying::setup(SceneManager &mgr) { ); } +void LevelPlaying::pause(SceneManager &mgr) noexcept { + _paused = true; + mgr.bgm.setVolume(0.15f); +} + +void LevelPlaying::unpause(SceneManager &mgr) noexcept { + _paused = false; + mgr.bgm.setVolume(1.0f); +} + void LevelPlaying::handleEvent(SceneManager &mgr, sf::Event &ev) { + if (_paused) { + if (auto kb = ev.getIf()) { + if (_show_help) { + switch (kb->code) { + case sf::Keyboard::Key::Escape: + case sf::Keyboard::Key::Enter: + case sf::Keyboard::Key::Space: + UISounds::instance().backward.play(); + _show_help = false; + return; + } + } else { + switch (kb->code) { + case sf::Keyboard::Key::Escape: + unpause(mgr); + break; + + case sf::Keyboard::Key::Enter: + case sf::Keyboard::Key::Space: + switch (_paused_menu_current_button_index) { + case PausedMenuButton::RESUME: + unpause(mgr); + return; + + case PausedMenuButton::HELP: + _show_help = true; + return; + + case PausedMenuButton::RETRY: + _restartLevel(mgr, false); + return; + + case PausedMenuButton::QUIT: + mgr.changeScene( + pro::make_proxy() + ); + return; + + default: + // Erroneous state? + _paused_menu_current_button_index = PausedMenuButton:: + RESUME; + break; + } + break; + + case sf::Keyboard::Key::Up: + case sf::Keyboard::Key::W: + UISounds::instance().backward.play(); + _paused_menu_current_button_index--; + if (_paused_menu_current_button_index < 0) { + _paused_menu_current_button_index + = PausedMenuButton::BUTTON_COUNT - 1; + } + break; + + case sf::Keyboard::Key::Down: + case sf::Keyboard::Key::S: + UISounds::instance().forward.play(); + _paused_menu_current_button_index++; + if (_paused_menu_current_button_index + >= PausedMenuButton::BUTTON_COUNT) { + _paused_menu_current_button_index = 0; + } + break; + + default: + break; + } + } + } + + // Ignore other events when paused + return; + } + constexpr int retry_cooldown_ticks = 24 * 2; if (auto mw = ev.getIf()) { @@ -96,8 +195,12 @@ void LevelPlaying::handleEvent(SceneManager &mgr, sf::Event &ev) { if (auto kb = ev.getIf()) { constexpr int min_num_key = static_cast(sf::Keyboard::Key::Num1); constexpr int max_num_key = static_cast(sf::Keyboard::Key::Num9); - constexpr int min_numpad_key = static_cast(sf::Keyboard::Key::Numpad1); - constexpr int max_numpad_key = static_cast(sf::Keyboard::Key::Numpad9); + constexpr int min_numpad_key = static_cast( + sf::Keyboard::Key::Numpad1 + ); + constexpr int max_numpad_key = static_cast( + sf::Keyboard::Key::Numpad9 + ); const int key_code = static_cast(kb->code); if (key_code >= min_num_key && key_code <= max_num_key) { @@ -127,15 +230,7 @@ void LevelPlaying::handleEvent(SceneManager &mgr, sf::Event &ev) { break; case sf::Keyboard::Key::Escape: - if (_hint_opacity > 0 && _hint_type == HintType::QuitLevel) { - mgr.changeScene( - pro::make_proxy() - ); - return; - } else { - _hint_type = HintType::QuitLevel; - _hint_opacity = hint_max_opacity; - } + pause(mgr); break; case sf::Keyboard::Key::Up: @@ -192,11 +287,75 @@ void LevelPlaying::render( sf::Color text_color = ui_text_color(_hint_opacity); font.renderText(target, hint_text, text_color, x, y, scale); } + + // Render Paused Menu + if (_paused) { + sf::RectangleShape overlay_mask; + overlay_mask.setSize( + sf::Vector2f(_level.width() * scale, _level.height() * scale) + ); + overlay_mask.setFillColor(sf::Color(0, 0, 0, 200)); + target.draw(overlay_mask); + + // Render "PAUSED" text + const std::string pause_text = "PAUSED"; + int x = (_level.width() - pause_text.size() * font.charWidth() * 2) / 2; + int y = 64; + font.renderText( + target, pause_text, sf::Color(255, 255, 255, 255), x, y, scale, 2 + ); + + // Render Buttons + auto renderButton = [&](std::string_view label, + const ButtonDescriptor &desc, bool is_active) { + sf::Color color = is_active ? ui_active_color + : ui_text_bright_color(255); + font.renderText( + target, std::string(label), color, desc.x, desc.y, scale, + desc.size + ); + }; + + const auto _button_list = std::array< + std::pair, PausedMenuButton::BUTTON_COUNT>{ + std::make_pair("Resume", PausedMenuButton::RESUME), + std::make_pair("Retry", PausedMenuButton::RETRY), + std::make_pair("Help", PausedMenuButton::HELP), + std::make_pair("Quit", PausedMenuButton::QUIT) + }; + + for (std::size_t i = 0; i < _button_list.size(); i++) { + const auto button = _button_list[i]; + + // Center the buttons horizontally + int button_x = (_level.width() + - button.first.size() * font.charWidth()) + / 2; + int button_y = (_level.height() / 2) + i * (font.charHeight() + 5); + + renderButton( + button.first, ButtonDescriptor{button_x, button_y, 1}, + _paused_menu_current_button_index == button.second + ); + } + } + + // Render help Overlay + if (_show_help) { + sf::Sprite help_sprite(*_help_texture); + help_sprite.setPosition(sf::Vector2f(0, 0)); + help_sprite.setScale(sf::Vector2f(scale, scale)); + + target.draw(help_sprite); + } } void LevelPlaying::step(SceneManager &mgr) { - _tick += 1; - _level.step(); + // Only step the level when not paused + if (!_paused) { + _tick += 1; + _level.step(); + } if (_hint_opacity > 0) { _hint_opacity -= hint_fade_speed; diff --git a/src/scenes/main_menu.cpp b/src/scenes/main_menu.cpp index ff97432..ef86cf8 100644 --- a/src/scenes/main_menu.cpp +++ b/src/scenes/main_menu.cpp @@ -2,6 +2,7 @@ #include "wforge/audio.h" #include "wforge/save.h" #include "wforge/scene.h" +#include "wforge/colorpalette.h" #include #include #include @@ -14,6 +15,7 @@ namespace { enum MainMenuButton { PLAY = 0, SETTINGS, + HELP, EXIT, BUTTON_COUNT }; @@ -54,19 +56,12 @@ MainMenu::MainMenu() desc.x = data.at("x"); desc.y = data.at("y"); desc.size = data.at("size"); - desc.color = sf::Color( - data.at("color").at(0), data.at("color").at(1), - data.at("color").at(2), data.at("color").at(3) - ); - desc.active_color = sf::Color( - data.at("active-color").at(0), data.at("active-color").at(1), - data.at("active-color").at(2), data.at("active-color").at(3) - ); return desc; }; _play_button = parseButtonDescriptor(buttons.at("play")); _settings_button = parseButtonDescriptor(buttons.at("settings")); + _help_button = parseButtonDescriptor(buttons.at("help")); _exit_button = parseButtonDescriptor(buttons.at("exit")); _version_text = UITextDescriptor::fromJson(json_data.at("version-text")); @@ -114,12 +109,16 @@ void MainMenu::handleEvent(SceneManager &mgr, sf::Event &evt) { mgr.changeScene(pro::make_proxy()); return; + case MainMenuButton::HELP: + mgr.changeScene(pro::make_proxy()); + return; + case MainMenuButton::EXIT: std::exit(0); return; default: - // Errorneous state? + // Erroneous state? _current_button_index = MainMenuButton::PLAY; break; } @@ -151,7 +150,7 @@ void MainMenu::render( // Render buttons auto renderButton = [&](std::string_view label, const ButtonDescriptor &desc, bool is_active) { - sf::Color color = is_active ? desc.active_color : desc.color; + sf::Color color = is_active ? ui_active_color : ui_text_color(255); font.renderText( target, std::string(label), color, desc.x, desc.y, scale, desc.size ); @@ -159,7 +158,7 @@ void MainMenu::render( auto &save = SaveData::instance(); renderButton( - save.completed_levels >= 0 ? "Play" : "New Game", _play_button, + save.is_first_launch() ? "New Game" : "Play", _play_button, _current_button_index == MainMenuButton::PLAY ); @@ -168,6 +167,11 @@ void MainMenu::render( _current_button_index == MainMenuButton::SETTINGS ); + renderButton( + "Help", _help_button, + _current_button_index == MainMenuButton::HELP + ); + renderButton( "Exit", _exit_button, _current_button_index == MainMenuButton::EXIT ); diff --git a/src/scenes/settings.cpp b/src/scenes/settings.cpp index 639110c..91eb914 100644 --- a/src/scenes/settings.cpp +++ b/src/scenes/settings.cpp @@ -233,18 +233,6 @@ SettingsMenu::SettingsMenu() _option_text_size = option_data.at("size"); _option_width = option_data.at("width"); - _option_color = sf::Color( - option_data.at("color").at(0), option_data.at("color").at(1), - option_data.at("color").at(2), option_data.at("color").at(3) - ); - - _option_active_color = sf::Color( - option_data.at("active-color").at(0), - option_data.at("active-color").at(1), - option_data.at("active-color").at(2), - option_data.at("active-color").at(3) - ); - _options.push_back(std::make_unique()); _options.push_back(std::make_unique()); _options.push_back(std::make_unique()); @@ -386,8 +374,8 @@ void SettingsMenu::render( for (size_t i = 0; i < _options.size(); ++i) { const auto &option = _options[i]; sf::Color color = (i == _current_option_index) - ? _option_active_color - : _option_color; + ? ui_active_color + : ui_text_color(255); std::string option_text = option->displayText(); std::string value_text = option->valueText();