diff --git a/data/images/autotiles.satc b/data/images/autotiles.satc index 5e104f1ec1..d5c8b93e39 100644 --- a/data/images/autotiles.satc +++ b/data/images/autotiles.satc @@ -10849,6 +10849,56 @@ ) + + (autotileset + (name "industrial_horiz") + (default 3170) + (autotile + (id 3183) + (solid #t) + (mask "***01***") + ) + (autotile + (id 4623) + (solid #t) + (mask "***11***") + ) + (autotile + (id 3184) + (solid #t) + (mask "***10***") + ) + (autotile + (id 3170) + (solid #t) + (mask "***00***") + ) + ) + + (autotileset + (name "industrial_vert") + (default 3170) + (autotile + (id 3189) + (solid #t) + (mask "*0****1*") + ) + (autotile + (id 4627) + (solid #t) + (mask "*1****1*") + ) + (autotile + (id 3190) + (solid #t) + (mask "*1****0*") + ) + (autotile + (id 3170) + (solid #t) + (mask "*0****0*") + ) + ) ;; SUPPORT MULTIPLE AUTOTILESETS PER TILE - END ) diff --git a/data/images/engine/editor/grid_button.png b/data/images/engine/editor/grid_button.png new file mode 100644 index 0000000000..f1d1466dec Binary files /dev/null and b/data/images/engine/editor/grid_button.png differ diff --git a/data/images/engine/editor/play_button.png b/data/images/engine/editor/play_button.png new file mode 100644 index 0000000000..9d3bb1ee92 Binary files /dev/null and b/data/images/engine/editor/play_button.png differ diff --git a/data/images/engine/editor/redo.png b/data/images/engine/editor/redo.png index dd4ab3d51e..d3467babfb 100644 Binary files a/data/images/engine/editor/redo.png and b/data/images/engine/editor/redo.png differ diff --git a/data/images/engine/editor/save.png b/data/images/engine/editor/save.png new file mode 100644 index 0000000000..801dd1ebfd Binary files /dev/null and b/data/images/engine/editor/save.png differ diff --git a/data/images/engine/editor/shadow.png b/data/images/engine/editor/shadow.png new file mode 100644 index 0000000000..05e86778ad Binary files /dev/null and b/data/images/engine/editor/shadow.png differ diff --git a/data/images/engine/editor/shadow2.png b/data/images/engine/editor/shadow2.png new file mode 100644 index 0000000000..0d1b93cffa Binary files /dev/null and b/data/images/engine/editor/shadow2.png differ diff --git a/data/images/engine/editor/toggle_tile_object_mode.png b/data/images/engine/editor/toggle_tile_object_mode.png new file mode 100644 index 0000000000..0ae0f7cf4c Binary files /dev/null and b/data/images/engine/editor/toggle_tile_object_mode.png differ diff --git a/data/images/engine/editor/undo.png b/data/images/engine/editor/undo.png index b76d93cd74..95c326d458 100644 Binary files a/data/images/engine/editor/undo.png and b/data/images/engine/editor/undo.png differ diff --git a/data/shader/shader330.frag b/data/shader/shader330.frag index 291be7547c..2ee8234462 100644 --- a/data/shader/shader330.frag +++ b/data/shader/shader330.frag @@ -9,6 +9,7 @@ uniform float game_time; uniform vec2 animate; uniform vec2 displacement_animate; uniform bool is_displacement; +uniform int blur; in vec4 diffuse_var; in vec2 texcoord_var; @@ -17,10 +18,33 @@ out vec4 fragColor; void main(void) { - if (backbuffer == 0.0 || !is_displacement) + if (backbuffer == 0.0 || !is_displacement || blur != 0.0) { - vec4 color = diffuse_var * texture(diffuse_texture, texcoord_var.st + (animate * game_time)); - fragColor = color; + vec4 color = diffuse_var * texture(diffuse_texture, texcoord_var.st + (animate * game_time)); + if (blur != 0.0) + { + vec2 uv = (fragcoord2uv * gl_FragCoord.xyw).xy; + uv.y = 1.0 - uv.y; + vec2 texel = vec2(fragcoord2uv[0].x, fragcoord2uv[1].y); + float num = 0.0; + vec4 sum = vec4(0.0); + + for (float y = -blur; y <= blur; ++y) + { + for (float x = -blur; x <= blur; ++x) + { + vec2 offset = vec2(x, y) * texel; + sum += texture(framebuffer_texture, uv + offset); + num++; + } + } + + fragColor = vec4(mix(color.rgb, (sum/num).rgb * 1.05, color.a), 1.0); + } + else + { + fragColor = color; + } } else if (is_displacement) { diff --git a/src/badguy/badguy.cpp b/src/badguy/badguy.cpp index 6a82c21486..6007da83b1 100644 --- a/src/badguy/badguy.cpp +++ b/src/badguy/badguy.cpp @@ -1267,8 +1267,10 @@ BadGuy::get_settings() ObjectSettings result = MovingSprite::get_settings(); if (!get_allowed_directions().empty()) - result.add_direction(_("Direction"), &m_start_dir, get_allowed_directions(), "direction"); - result.add_script(_("Death script"), &m_dead_script, "dead-script"); + result.add_direction(_("Direction"), &m_start_dir, get_allowed_directions(), "direction") + ->set_description(_("The direction the badguy is facing at the start of the level. AUTO makes the badguy face towards the player when activated.")); + result.add_script(get_uid(), _("Death script"), &m_dead_script, "dead-script") + ->set_description(_("Script that is executed when the badguy dies.")); result.reorder({"direction", "sprite", "x", "y"}); diff --git a/src/badguy/boss.cpp b/src/badguy/boss.cpp index 0de39d3a91..639ca4f339 100644 --- a/src/badguy/boss.cpp +++ b/src/badguy/boss.cpp @@ -22,6 +22,7 @@ namespace #include "badguy/boss.hpp" +#include "editor/editor.hpp" #include "object/player.hpp" #include "sprite/sprite.hpp" #include "supertux/sector.hpp" @@ -67,7 +68,7 @@ Boss::draw(DrawingContext& context) void Boss::draw_hit_points(DrawingContext& context) { - if (m_hud_head) + if (m_hud_head && !Editor::is_active()) { context.push_transform(); context.set_translation(Vector(0, 0)); @@ -91,19 +92,23 @@ Boss::get_settings() { ObjectSettings result = BadGuy::get_settings(); - result.add_text("hud-icon", &m_hud_icon, "hud-icon", "images/creatures/yeti/hudlife.png", OPTION_HIDDEN); - result.add_int(_("Lives"), &m_lives, "lives", DEFAULT_LIVES); + result.add_text("hud-icon", &m_hud_icon, "hud-icon", "images/creatures/yeti/hudlife.png", OPTION_HIDDEN) + ->set_description(_("The icon that is displayed at the top indicating how many lives this boss has left")); + result.add_int(_("Lives"), &m_lives, "lives", DEFAULT_LIVES) + ->set_description(_("The maximum number of lives this boss has")); /* l10n: Pinch Mode refers to a particular boss mode that gets activated once the boss has lost the specified amounts of lives. This setting specifies how many lives need to be spent until pinch mode is activated. */ - result.add_int(_("Lives to Pinch Mode"), &m_pinch_lives, "pinch-lives", DEFAULT_PINCH_LIVES); + result.add_int(_("Lives to Pinch Mode"), &m_pinch_lives, "pinch-lives", DEFAULT_PINCH_LIVES) + ->set_description(_("Specifies how many lives need to be spent until pinch mode (a special boss mode) is activated")); /* l10n: Pinch Mode refers to a particular boss mode that gets activated once the boss has lost the specified amounts of lives. This setting specifies the squirrel script that gets run to activate boss mode. */ - result.add_script(_("Pinch Mode Activation Script"), &m_pinch_activation_script, "pinch-activation-script"); + result.add_script(get_uid(), _("Pinch Mode Activation Script"), &m_pinch_activation_script, "pinch-activation-script") + ->set_description(_("The script that gets run when pinch mode is activated")); return result; } diff --git a/src/badguy/crusher.cpp b/src/badguy/crusher.cpp index 59792f718b..2023316c20 100644 --- a/src/badguy/crusher.cpp +++ b/src/badguy/crusher.cpp @@ -1229,7 +1229,7 @@ Crusher::get_settings() static_cast(CrusherDirection::DOWN), "direction"); - result.add_script(_("Crush script"), &m_crush_script, "crush-script"); + result.add_script(get_uid(), _("Crush script"), &m_crush_script, "crush-script"); result.reorder({ "direction", "sprite", "crush-script", "x", "y" }); return result; diff --git a/src/badguy/darttrap.cpp b/src/badguy/darttrap.cpp index 1d6480fb9f..69b3912b97 100644 --- a/src/badguy/darttrap.cpp +++ b/src/badguy/darttrap.cpp @@ -152,11 +152,16 @@ DartTrap::get_settings() { ObjectSettings result = StickyBadguy::get_settings(); - result.add_float(_("Initial delay"), &m_initial_delay, "initial-delay"); - result.add_bool(_("Enabled"), &m_enabled, "enabled", true); - result.add_float(_("Fire delay"), &m_fire_delay, "fire-delay"); - result.add_int(_("Ammo"), &m_ammo, "ammo"); - result.add_sprite(_("Dart sprite"), &m_dart_sprite, "dart-sprite", "images/creatures/darttrap/granito/root_dart.sprite"); + result.add_float(_("Initial delay"), &m_initial_delay, "initial-delay") + ->set_description(_("Time until the first dart is fired after the trap is activated.")); + result.add_bool(_("Enabled"), &m_enabled, "enabled", true) + ->set_description(_("Whether the trap is enabled.")); + result.add_float(_("Fire delay"), &m_fire_delay, "fire-delay") + ->set_description(_("Time between consecutive darts.")); + result.add_int(_("Ammo"), &m_ammo, "ammo") + ->set_description(_("Number of darts the trap can fire. A value of -1 means infinite.")); + result.add_sprite(_("Dart sprite"), &m_dart_sprite, "dart-sprite", "images/creatures/darttrap/granito/root_dart.sprite") + ->set_description(_("Sprite used for the dart.")); result.reorder({"initial-delay", "fire-delay", "ammo", "sticky", "direction", "x", "y", "dart-sprite"}); diff --git a/src/badguy/granito.cpp b/src/badguy/granito.cpp index eba26b5814..755b73ffed 100644 --- a/src/badguy/granito.cpp +++ b/src/badguy/granito.cpp @@ -279,8 +279,8 @@ Granito::get_settings() settings.remove("dead-script"); - settings.add_script(_("Detect script"), &m_detect_script, "detect-script"); - settings.add_script(_("Carried script"), &m_carried_script, "carried-script"); + settings.add_script(get_uid(), _("Detect script"), &m_detect_script, "detect-script"); + settings.add_script(get_uid(), _("Carried script"), &m_carried_script, "carried-script"); return settings; } diff --git a/src/badguy/granito_big.cpp b/src/badguy/granito_big.cpp index 2509fd25f7..6935d5e82c 100644 --- a/src/badguy/granito_big.cpp +++ b/src/badguy/granito_big.cpp @@ -62,7 +62,7 @@ GranitoBig::get_settings() // No need to make another member for the carrying script. // Just repurpose the carried script. - settings.add_script(_("Carrying Script"), &m_carried_script, "carrying-script"); + settings.add_script(get_uid(), _("Carrying Script"), &m_carried_script, "carrying-script"); return settings; } diff --git a/src/editor/button_widget.cpp b/src/editor/button_widget.cpp index 0fd91ac84e..cead45b519 100644 --- a/src/editor/button_widget.cpp +++ b/src/editor/button_widget.cpp @@ -18,37 +18,62 @@ #include "supertux/gameconfig.hpp" #include "supertux/globals.hpp" +#include "supertux/resources.hpp" +#include "util/log.hpp" #include "video/viewport.hpp" #include "video/video_system.hpp" ButtonWidget::ButtonWidget(SpritePtr sprite, const Vector& pos, - std::function sig_click) : + std::function sig_click, std::optional sprite_size) : m_sprite(std::move(sprite)), - m_rect(pos, Sizef(static_cast(m_sprite->get_width()), - static_cast(m_sprite->get_height()))), + m_rect(pos, sprite_size ? *sprite_size : + Sizef(static_cast(m_sprite->get_width()), + static_cast(m_sprite->get_height()))), m_grab(false), m_hover(false), - m_sig_click(std::move(sig_click)) + m_sig_click(std::move(sig_click)), + m_mouse_pos(), + m_help_text(), + m_flat(false), + m_disabled(false) { } +void +ButtonWidget::set_position(const Vector& pos) +{ + float w = m_rect.get_width(), + h = m_rect.get_height(); + + m_rect.set_left(pos.x); + m_rect.set_width(w); + m_rect.set_top(pos.y); + m_rect.set_height(h); +} + void ButtonWidget::draw(DrawingContext& context) { - context.color().draw_filled_rect(m_rect, Color(0.0f, 0.0f, 0.0f, 0.6f), 4.0f, - LAYER_GUI-5); + if (!m_flat) + context.color().draw_filled_rect(m_rect, Color(0.0f, 0.0f, 0.0f, 0.6f), 4.0f, + LAYER_GUI-5); if (m_sprite) { - m_sprite->draw(context.color(), m_rect.p1(), LAYER_GUI-5); + if (m_disabled) context.set_alpha(0.4); + m_sprite->draw_scaled(context.color(), m_rect.grown(-4.0f), LAYER_GUI - 5); + if (m_disabled) context.set_alpha(1.0); } if (m_grab) { context.color().draw_filled_rect(m_rect, g_config->editorgrabcolor, 4.0f, LAYER_GUI-5); - } else if (m_hover) { + } else if (m_hover && !m_disabled) { context.color().draw_filled_rect(m_rect, g_config->editorhovercolor, 4.0f, LAYER_GUI-5); } + + if (m_hover && !m_help_text.empty()) + context.color().draw_text(Resources::normal_font, m_help_text, m_mouse_pos + Vector(32, 32), FontAlignment::ALIGN_LEFT, INT_MAX); } void @@ -70,11 +95,13 @@ bool ButtonWidget::on_mouse_button_up(const SDL_MouseButtonEvent& button) { if (button.button != SDL_BUTTON_LEFT) return false; - - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + // XXX: these shouldn't pass through + if (m_disabled) return false; + + m_mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); if (m_grab) { - if (m_rect.contains(mouse_pos)) { + if (m_rect.contains(m_mouse_pos)) { if (m_sig_click) { m_sig_click(); } @@ -90,11 +117,13 @@ ButtonWidget::on_mouse_button_up(const SDL_MouseButtonEvent& button) bool ButtonWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) { - if (button.button != SDL_BUTTON_LEFT) return false; + if (button.button != SDL_BUTTON_LEFT || m_disabled) return false; + // XXX: these shouldn't pass through + if (m_disabled) return false; - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + m_mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); - if (m_rect.contains(mouse_pos)) { + if (m_rect.contains(m_mouse_pos)) { m_hover = true; m_grab = true; return true; @@ -107,12 +136,12 @@ ButtonWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) bool ButtonWidget::on_mouse_motion(const SDL_MouseMotionEvent& motion) { - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(motion.x, motion.y); + m_mouse_pos = VideoSystem::current()->get_viewport().to_logical(motion.x, motion.y); if (m_grab) { - m_hover = m_rect.contains(mouse_pos); + m_hover = m_rect.contains(m_mouse_pos); return true; - } else if (m_rect.contains(mouse_pos)) { + } else if (m_rect.contains(m_mouse_pos)) { m_hover = true; return false; } else { @@ -120,3 +149,22 @@ ButtonWidget::on_mouse_motion(const SDL_MouseMotionEvent& motion) return false; } } + +void +ButtonWidget::set_sprite(SpritePtr sprite) +{ + m_sprite = std::move(sprite); + m_rect.set_size(sprite->get_width() * 1.0f, sprite->get_height() * 1.0f); +} + +void +ButtonWidget::set_sprite(const std::string& path) +{ + set_sprite(SpriteManager::current()->create(path)); +} + +void +ButtonWidget::set_help_text(const std::string& help_text) +{ + m_help_text = help_text; +} diff --git a/src/editor/button_widget.hpp b/src/editor/button_widget.hpp index 94ab07546a..b33e79ff9d 100644 --- a/src/editor/button_widget.hpp +++ b/src/editor/button_widget.hpp @@ -26,14 +26,14 @@ class ButtonWidget : public Widget { -private: public: - ButtonWidget(SpritePtr sprite, const Vector& pos, std::function m_sig_click = {}); - ButtonWidget(const std::string& path, const Vector& pos, std::function callback = {}) : - ButtonWidget(SpriteManager::current()->create(path), pos, std::move(callback)) + ButtonWidget(SpritePtr sprite, const Vector& pos, std::function m_sig_click = {}, std::optional sprite_size = std::nullopt); + ButtonWidget(const std::string& path, const Vector& pos, std::function callback = {}, std::optional sprite_size = std::nullopt) : + ButtonWidget(SpriteManager::current()->create(path), std::move(pos), std::move(callback), std::move(sprite_size)) { } + virtual void draw(DrawingContext& context) override; virtual void update(float dt_sec) override; @@ -44,14 +44,117 @@ class ButtonWidget : public Widget virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override; virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override; -private: + void set_position(const Vector& pos); + void set_sprite(const std::string& path); + void set_sprite(SpritePtr sprite); + + void set_help_text(const std::string& help_text); + + inline void set_disabled(bool disabled) { m_disabled = disabled; } + inline bool is_disabled() { return m_disabled; } + + inline void set_flat(bool flat) { m_flat = flat; } + inline bool is_flat() { return m_flat; } + +protected: SpritePtr m_sprite; Rectf m_rect; bool m_grab; bool m_hover; + bool m_disabled; + bool m_flat; std::function m_sig_click; + Vector m_mouse_pos; + std::string m_help_text; private: ButtonWidget(const ButtonWidget&) = delete; ButtonWidget& operator=(const ButtonWidget&) = delete; }; + +class EditorToolbarButtonWidget : public ButtonWidget +{ +public: + EditorToolbarButtonWidget(SpritePtr sprite, const Vector& pos, std::function m_sig_click = {}, std::optional sprite_size = std::nullopt) : + ButtonWidget(std::move(sprite), std::move(pos), m_sig_click, std::move(sprite_size)), + m_tile_mode_visible(true), + m_object_mode_visible(true), + m_visible(true) + { + } + + EditorToolbarButtonWidget(const std::string& path, const Vector& pos, std::function callback = {}, std::optional sprite_size = std::nullopt) : + ButtonWidget(SpriteManager::current()->create(path), std::move(pos), std::move(callback), std::move(sprite_size)), + m_tile_mode_visible(true), + m_object_mode_visible(true), + m_visible(true) + { + } + + EditorToolbarButtonWidget(const std::string& path, std::function callback = {}, std::string help_text = "", std::optional sprite_size = std::nullopt) : + ButtonWidget(SpriteManager::current()->create(path), Vector(0,0), std::move(callback), std::move(sprite_size)), + m_tile_mode_visible(true), + m_object_mode_visible(true), + m_visible(true) + { + set_help_text(help_text); + } + + virtual void draw(DrawingContext& context) override + { + if (!get_visible()) + return; + + ButtonWidget::draw(context); + } + + virtual void update(float dt_sec) override + { + if (!get_visible()) + return; + + ButtonWidget::update(dt_sec); + } + + virtual bool on_mouse_button_up(const SDL_MouseButtonEvent& button) override + { + if (!get_visible()) + return false; + + return ButtonWidget::on_mouse_button_up(button); + } + + virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override + { + if (!get_visible()) + return false; + + return ButtonWidget::on_mouse_button_down(button); + } + + virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override + { + if (!get_visible()) + return false; + + return ButtonWidget::on_mouse_motion(motion); + } + + void set_visible_in_tile_mode(bool visible) { m_tile_mode_visible = visible; } + bool get_visible_in_tile_mode() const { return m_tile_mode_visible; } + + void set_visible_in_object_mode(bool visible) { m_object_mode_visible = visible; } + bool get_visible_in_object_mode() const { return m_object_mode_visible; } + + void set_visible(bool visible) { m_visible = visible; } + bool get_visible() const { return m_visible; } + +private: + bool m_tile_mode_visible; + bool m_object_mode_visible; + bool m_visible; + +private: + EditorToolbarButtonWidget(const EditorToolbarButtonWidget&) = delete; + EditorToolbarButtonWidget& operator=(const EditorToolbarButtonWidget&) = delete; +}; diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 33cd5f86e4..ce0a77a956 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -15,8 +15,11 @@ // along with this program. If not, see . #include "editor/editor.hpp" +#include "gui/notification.hpp" +#include "math/rectf.hpp" #include +#include #include #include #include @@ -42,6 +45,7 @@ #include "editor/tool_icon.hpp" #include "gui/dialog.hpp" #include "gui/menu_manager.hpp" +#include "gui/menu_script.hpp" #include "gui/mousecursor.hpp" #include "math/util.hpp" #include "object/camera.hpp" @@ -74,6 +78,8 @@ #include "video/surface.hpp" #include "video/video_system.hpp" #include "video/viewport.hpp" +#include "supertux/sector.hpp" +#include "supertux/sector_parser.hpp" static const float CAMERA_MIN_ZOOM = 0.5f; static const float CAMERA_MAX_ZOOM = 3.0f; @@ -82,6 +88,8 @@ static const float CAMERA_ZOOM_FOCUS_PROGRESSION = 8.f; bool Editor::s_resaving_in_progress = false; +using InputType = EditorTilebox::InputType; + bool Editor::is_active() { @@ -93,6 +101,23 @@ Editor::is_active() } } +void +Editor::may_deactivate() +{ + auto* self = Editor::current(); + if (self) + self->m_deactivate_request = true; +} + +void +Editor::may_reactivate() +{ + auto* self = Editor::current(); + if (self) + self->m_reactivate_request = true; +} + + Editor::Editor() : m_level(), m_world(), @@ -109,8 +134,11 @@ Editor::Editor() : m_test_request(false), m_particle_editor_request(false), m_test_pos(), + m_temp_level(true), m_particle_editor_filename(), m_ctrl_pressed(false), + m_shift_pressed(false), + m_alt_pressed(false), m_sector(), m_levelloaded(false), m_leveltested(false), @@ -118,18 +146,28 @@ Editor::Editor() : m_tileset(nullptr), m_has_deprecated_tiles(false), m_widgets(), + m_widgets_width(), + m_widgets_width_offset(), + m_controls(), m_undo_widget(), m_redo_widget(), m_overlay_widget(), m_toolbox_widget(), m_layers_widget(), + m_selected_object(), + m_testing_disabled(false), m_enabled(false), m_bgr_surface(Surface::from_file("images/engine/menu/bg_editor.png")), m_time_since_last_save(0.f), m_scroll_speed(32.0f), m_new_scale(0.f), m_mouse_pos(0.f, 0.f), - m_layers_widget_needs_refresh(false) + m_layers_widget_needs_refresh(false), + m_script_manager(), + m_on_exit_cb(nullptr), + m_save_temp_level(false), + m_last_test_pos(std::nullopt), + m_shadow(SpriteManager::current()->create("images/engine/editor/shadow.png")) { auto toolbox_widget = std::make_unique(*this); auto layers_widget = std::make_unique(*this); @@ -142,10 +180,187 @@ Editor::Editor() : m_widgets.push_back(std::move(toolbox_widget)); m_widgets.push_back(std::move(layers_widget)); m_widgets.push_back(std::move(overlay_widget)); + + std::array, 8> general_widgets = { + // Undo button + std::make_unique("images/engine/editor/undo.png", + std::bind(&Editor::undo, this), + _("Undo"), + Sizef(32.f, 32.f)), + + std::make_unique("images/engine/editor/redo.png", + std::bind(&Editor::redo, this), + _("Redo"), + Sizef(32.f, 32.f)), + + // Grid button + std::make_unique("images/engine/editor/grid_button.png", + [this] { + auto& snap_grid_size = g_config->editor_selected_snap_grid_size; + if (snap_grid_size == 0) + { + if(!g_config->editor_render_grid) + { + snap_grid_size = 3; + } + g_config->editor_render_grid = !g_config->editor_render_grid; + } + else + snap_grid_size--; + }, + _("Change / Toggle grid size")), + + // Play button + std::make_unique("images/engine/editor/play_button.png", + [this] { m_test_request = true; }, + _("Test level")), + + // Save button + std::make_unique("images/engine/editor/save.png", + [this] { + if (save_level()) + { + auto notif = std::make_unique("save_level_notif", 5.f); + notif->set_text(_("Level saved!")); + MenuManager::instance().set_notification(std::move(notif)); + } + }, + _("Save level")), + + // Mode button + std::make_unique("images/engine/editor/toggle_tile_object_mode.png", + std::bind(&Editor::toggle_tile_object_mode, this), + _("Toggle between object and tile mode")), + + // Mouse select button + std::make_unique("images/engine/editor/arrow.png", + [this]() { + m_toolbox_widget->set_mouse_tool(); + }, + _("Toggle between add and remove mode")), + + // Rubber button + std::make_unique("images/engine/editor/rubber.png", + [this]() { + m_toolbox_widget->set_rubber_tool(); + }, + _("Delete the tile or object under the mouse")) + }; + + std::array, 4> tile_mode_widgets = { + // Select mode mouse + std::make_unique("images/engine/editor/select-mode0.png", + [this] { + m_toolbox_widget->set_tileselect_select_mode(0); + }, + _("Draw mode (The current tool applies to the tile under the mouse)")), + + // Select mode area + std::make_unique("images/engine/editor/select-mode1.png", + [this] { + m_toolbox_widget->set_tileselect_select_mode(1); + }, + _("Box draw mode (The current tool applies to an area / box drawn with the mouse)")), + + // Select mode fill button + std::make_unique("images/engine/editor/select-mode2.png", + [this] { + m_toolbox_widget->set_tileselect_select_mode(2); + }, + _("Fill mode (The current tool applies to the empty area in the enclosed space that was clicked)")), + + // Select mode same button + std::make_unique("images/engine/editor/select-mode3.png", + [this] { + m_toolbox_widget->set_tileselect_select_mode(3); + }, + _("Replace mode (The current tool applies to all tiles that are the same tile as the one under the mouse)")), + }; + + std::array, 2> object_mode_widgets = { + // Select mode + std::make_unique("images/engine/editor/move-mode0.png", + [this] { + m_toolbox_widget->set_tileselect_move_mode(0); + }, + _("Select mode (Clicking selects the object under the mouse)")), + + // Duplicate mode + std::make_unique("images/engine/editor/move-mode1.png", + [this] { + m_toolbox_widget->set_tileselect_move_mode(1); + }, + _("Duplicate mode (Clicking duplicates the object under the mouse)")), + }; + + size_t i = 2; + for (auto &widget : general_widgets) + { + Vector pos(32 * (i-2), 0); + widget->set_position(pos); + widget->set_flat(true); + m_widgets.insert(m_widgets.begin() + i, std::move(widget)); + ++i; + } + + for (auto &widget : tile_mode_widgets) + { + Vector pos(32 * (i-2), 0); + widget->set_position(pos); + widget->set_flat(true); + widget->set_visible_in_object_mode(false); + widget->set_visible(false); + m_widgets.insert(m_widgets.begin() + i, std::move(widget)); + ++i; + } + + for (auto &widget : object_mode_widgets) + { + Vector pos(32 * (i-tile_mode_widgets.size()-2), 0); + widget->set_position(pos); + widget->set_flat(true); + widget->set_visible_in_tile_mode(false); + widget->set_visible(false); + m_widgets.insert(m_widgets.begin() + i, std::move(widget)); + ++i; + } + m_widgets_width = 32.f * (i-std::max(tile_mode_widgets.size(), object_mode_widgets.size())-2-2); + + m_undo_widget = reinterpret_cast(m_widgets[2].get()); + m_redo_widget = reinterpret_cast(m_widgets[3].get()); + m_undo_widget->set_disabled(true); + m_redo_widget->set_disabled(true); + + // auto code_widget = std::make_unique( + // "images/engine/editor/select-mode3.png", Vector(320, 0), [this] { + // std::ostringstream level_ostream; + // Writer output_writer(level_ostream); + // m_level->save(output_writer); + // auto level_content = level_ostream.str(); + // MenuManager::instance().push_menu(std::make_unique(&level_content)); + // log_warning << level_content << std::endl; + // }); + // m_widgets.insert(m_widgets.begin() + 10, std::move(code_widget)); } Editor::~Editor() { + if (m_on_exit_cb) + m_on_exit_cb(); +} + +void +Editor::level_from_nothing() +{ + m_level = std::make_unique(false); + m_level->m_name = ""; + m_level->m_license = LEVEL_DEFAULT_LICENSE; + m_level->m_tileset = "images/tiles.strf"; + auto sector = SectorParser::from_nothing(*m_level); + sector->set_name(DEFAULT_SECTOR_NAME); + m_level->add_sector(std::move(sector)); + m_level->initialize(); + //m_reload_request = true; } void @@ -159,10 +374,44 @@ Editor::draw(Compositor& compositor) { auto& context = compositor.make_context(); - if (m_levelloaded) { - for(const auto& widget : m_widgets) { + if (m_levelloaded) + { + context.color().set_blur(g_config->editor_blur); + context.color().draw_filled_rect( + {-g_config->menuroundness, -g_config->menuroundness, m_widgets_width + m_widgets_width_offset, 32}, + Color(0.2f, 0.2f, 0.2f, 0.5f), + math::clamp(g_config->menuroundness, 0.f, 16.f), + LAYER_GUI - 5); + context.color().set_blur(0); + + for(const auto& widget : m_widgets) + { + if (!g_config->editor_show_toolbar_widgets && + dynamic_cast(widget.get())) + { + continue; + } widget->draw(context); } + + m_overlay_widget->draw_tilemap_outer_shading(context); + m_overlay_widget->draw_tilemap_border(context); + + if (get_properties_panel_visible()) + { + context.color().set_blur(g_config->editor_blur); + context.color().draw_filled_rect(Rectf(0.0f, 0.0f, SCREEN_WIDTH, 32.0f), + Color(0.2f, 0.2f, 0.2f, 0.5f), LAYER_GUI - 6); + + context.color().draw_filled_rect(Rectf(0, 32.0f, 200.0f, SCREEN_HEIGHT - 32.0f), + Color(0.2f, 0.2f, 0.2f, 0.5f), LAYER_GUI - 6); + context.color().set_blur(0); + + for(const auto& control : m_controls) + { + control->draw(context); + } + } // If camera scale must be changed, change it here. if (m_new_scale != 0.f) @@ -190,8 +439,76 @@ Editor::draw(Compositor& compositor) // Avoid drawing the sector if we're about to test it, as there is a dangling pointer // issue with the PlayerStatus. if (!m_leveltested) + { m_sector->draw(context); + // If an object is selected, draw an indicator around it. + const GameObject* selected_object = m_selected_object.get(); + if (selected_object) + { + const MovingObject* moving_selected_obj = dynamic_cast(selected_object); + if (moving_selected_obj) + { + context.push_transform(); + const Camera& camera = m_sector->get_camera(); + context.set_translation(camera.get_translation()); + context.scale(camera.get_current_scale()); + + const Rectf& bbox = moving_selected_obj->get_bbox(); + context.color().draw_line(bbox.p1() - Vector(10.f, 10.f), bbox.p1() - Vector(0.f, 10.f), Color::WHITE, LAYER_GUI + 1); + context.color().draw_line(bbox.p1() - Vector(10.f, 10.f), bbox.p1() - Vector(10.f, 0.f), Color::WHITE, LAYER_GUI + 1); + context.color().draw_line(bbox.p2() + Vector(10.f, 10.f), bbox.p2() + Vector(0.f, 10.f), Color::WHITE, LAYER_GUI + 1); + context.color().draw_line(bbox.p2() + Vector(10.f, 10.f), bbox.p2() + Vector(10.f, 0.f), Color::WHITE, LAYER_GUI + 1); + context.color().draw_line(Vector(bbox.get_right() + 10.f, bbox.get_top() - 10.f), + Vector(bbox.get_right() + 10.f, bbox.get_top()), + Color::WHITE, LAYER_GUI + 1); + context.color().draw_line(Vector(bbox.get_right() + 10.f, bbox.get_top() - 10.f), + Vector(bbox.get_right(), bbox.get_top() - 10.f), + Color::WHITE, LAYER_GUI + 1); + context.color().draw_line(Vector(bbox.get_left() - 10.f, bbox.get_bottom() + 10.f), + Vector(bbox.get_left() - 10.f, bbox.get_bottom()), + Color::WHITE, LAYER_GUI + 1); + context.color().draw_line(Vector(bbox.get_left() - 10.f, bbox.get_bottom() + 10.f), + Vector(bbox.get_left(), bbox.get_bottom() + 10.f), + Color::WHITE, LAYER_GUI + 1); + + context.pop_transform(); + } + } + else + { + m_selected_object = 0; + m_controls.clear(); + } + } + + // BEGIN Draw shadows and line + constexpr float LINE_THICKNESS = 1.f; + Rectf border_rect = Rectf{SCREEN_WIDTH - 128.f - LINE_THICKNESS, 0, + SCREEN_WIDTH - 128.f, static_cast(SCREEN_HEIGHT - 32.f)}; + Color line_color = g_config->editorcolor; + line_color.red -= 0.2; + line_color.green -= 0.2; + line_color.blue -= 0.2; + line_color.alpha -= 0.2; + context.color().draw_filled_rect(border_rect, line_color, LAYER_GUI + 1); + + if (m_shadow) + { + Rectf shadow_rect = border_rect; + shadow_rect.set_left(border_rect.get_left() - 16 + LINE_THICKNESS); + shadow_rect.set_right(border_rect.get_right() - LINE_THICKNESS); + context.set_alpha(0.2); + m_shadow->draw_scaled(context.color(), shadow_rect, LAYER_GUI + 1); + context.set_alpha(1.0); + } + + + Rectf layers_rect = Rectf{0, SCREEN_HEIGHT - 32.f - LINE_THICKNESS, + SCREEN_WIDTH - 128.f, SCREEN_HEIGHT - 32.f}; + context.color().draw_filled_rect(layers_rect, line_color, LAYER_GUI + 1); + // END Draw shadows and line + context.color().draw_filled_rect(context.get_rect(), Color(0.0f, 0.0f, 0.0f), 0.0f, std::numeric_limits::min()); @@ -208,7 +525,7 @@ void Editor::update(float dt_sec, const Controller& controller) { // Auto-save (interval). - if (m_level) { + if (m_level && !m_temp_level) { m_time_since_last_save += dt_sec; if (m_time_since_last_save >= static_cast(std::max( g_config->editor_autosave_frequency, 1)) * 60.f) { @@ -233,6 +550,8 @@ Editor::update(float dt_sec, const Controller& controller) m_time_since_last_save = 0.f; } + m_script_manager.poll(); + // Pass all requests. if (m_reload_request) { reload_level(); @@ -246,9 +565,27 @@ Editor::update(float dt_sec, const Controller& controller) // Create new level. } + if (m_deactivate_request) { + m_enabled = false; + m_deactivate_request = false; + return; + } + if (m_reactivate_request) { - m_enabled = true; m_reactivate_request = false; + + if (!m_enabled) + { + // It's possible that the editor is being re-activated due to exiting a menu, + // possibly one related to an object option. + GameObject* selected_object = m_selected_object.get(); + if (selected_object) + { + selected_object->after_editor_set(); + selected_object->check_state(); + } + } + m_enabled = true; } if (m_save_request) { @@ -262,6 +599,7 @@ Editor::update(float dt_sec, const Controller& controller) if (m_test_request) { m_test_request = false; MouseCursor::current()->set_icon(nullptr); + m_last_test_pos = m_test_pos; test_level(m_test_pos); return; } @@ -275,12 +613,6 @@ Editor::update(float dt_sec, const Controller& controller) return; } - if (m_deactivate_request) { - m_enabled = false; - m_deactivate_request = false; - return; - } - // Update other components. if (m_levelloaded && !m_leveltested) { BIND_SECTOR(*m_sector); @@ -302,6 +634,11 @@ Editor::update(float dt_sec, const Controller& controller) widget->update(dt_sec); } + for(const auto& control : m_controls) + { + control->update(dt_sec); + } + // Now that all widgets have been updated, which should have relinquished // pointers to objects marked for deletion, we can actually delete them. for (auto& sector : m_level->get_sectors()) @@ -314,6 +651,9 @@ Editor::update(float dt_sec, const Controller& controller) void Editor::remove_autosave_file() { + if (m_temp_level) + return; + // Clear the auto-save file. if (!m_autosave_levelfile.empty()) { @@ -328,9 +668,23 @@ Editor::remove_autosave_file() } } -void +bool Editor::save_level(const std::string& filename, bool switch_file) { + if (m_temp_level && !m_save_temp_level) + { + MenuManager::instance().set_menu(MenuStorage::EDITOR_TEMP_SAVE_MENU); + return false; + } + + if (m_save_temp_level) + { + m_save_temp_level = false; + m_temp_level = false; + // Implied + switch_file = true; + } + auto file = !filename.empty() ? filename : m_levelfile; if (switch_file) @@ -343,6 +697,7 @@ Editor::save_level(const std::string& filename, bool switch_file) m_level->save(m_world ? FileSystem::join(m_world->get_basedir(), file) : file); m_time_since_last_save = 0.f; remove_autosave_file(); + return true; } std::string @@ -367,8 +722,23 @@ Editor::get_level_directory() const void Editor::test_level(const std::optional>& test_pos) { + if (m_testing_disabled) + { + Dialog::show_message(_("You cannot test a level when playing from the worldmap.\n\n" + "Exit the level editor instead.")); + return; + } + Tile::draw_editor_images = false; Compositor::s_render_lighting = true; + + m_leveltested = true; + if ((m_level && m_levelfile.empty()) || m_levelfile == "") + { + GameManager::current()->start_level(m_level.get(), test_pos, true); + return; + } + std::string backup_filename = get_autosave_from_levelname(m_levelfile); std::string directory = get_level_directory(); @@ -384,13 +754,12 @@ Editor::test_level(const std::optional>& test_pos m_autosave_levelfile = FileSystem::join(directory, backup_filename); m_level->save(m_autosave_levelfile); m_time_since_last_save = 0.f; - m_leveltested = true; if (!m_level->is_worldmap()) { // TODO: After LevelSetScreen is removed, this should return a boolean indicating whether load was successful. // If not, call reactivate(). - GameManager::current()->start_level(*current_world, backup_filename, test_pos); + GameManager::current()->start_level(*current_world, backup_filename, test_pos, true); } else if (!GameManager::current()->start_worldmap(*current_world, m_autosave_levelfile, test_pos)) { @@ -401,6 +770,8 @@ Editor::test_level(const std::optional>& test_pos void Editor::open_level_directory() { + if (m_temp_level) + return; m_level->save(FileSystem::join(get_level_directory(), m_levelfile)); auto path = FileSystem::join(PHYSFS_getWriteDir(), get_level_directory()); FileSystem::open_path(path); @@ -419,9 +790,12 @@ void Editor::keep_camera_in_bounds() { Camera& camera = m_sector->get_camera(); - camera.keep_in_bounds(Rectf(0.f, 0.f, - std::max(0.0f, m_sector->get_editor_width() + 128.f / camera.get_current_scale()), - std::max(0.0f, m_sector->get_editor_height() + 32.f / camera.get_current_scale()))); + constexpr float offset = 80.f; + float controls_offset_x = m_controls.size() != 0 ? -200.f : 0.f; + float controls_offset_y = m_controls.size() != 0 ? -32.f : 0.f; + camera.keep_in_bounds(Rectf(-offset + controls_offset_x, -offset + controls_offset_y, + std::max(0.0f, m_sector->get_editor_width() + 128.f / camera.get_current_scale()) + offset, + std::max(0.0f, m_sector->get_editor_height() + 32.f / camera.get_current_scale()) + offset)); m_overlay_widget->update_pos(); } @@ -437,9 +811,8 @@ Editor::esc_press() void Editor::update_keyboard(const Controller& controller) { - if (!m_enabled) { + if (!m_enabled) return; - } if (MenuManager::instance().is_active() || MenuManager::instance().has_dialog()) return; @@ -502,6 +875,7 @@ Editor::set_sector(Sector* sector) } m_layers_widget->refresh(); + select_object(nullptr); } void @@ -527,6 +901,8 @@ Editor::set_level(std::unique_ptr level, bool reset) { std::string sector_name = DEFAULT_SECTOR_NAME; Vector translation(0.0f, 0.0f); + + m_temp_level = (level == nullptr); if (!reset && m_sector) { translation = m_sector->get_camera().get_translation(); @@ -540,12 +916,18 @@ Editor::set_level(std::unique_ptr level, bool reset) m_toolbox_widget->get_tilebox().set_input_type(EditorTilebox::InputType::NONE); } - // Reload level. - m_level = nullptr; m_levelloaded = true; - m_level = std::move(level); - + if (level != nullptr) + { + // Reload level. + m_level = std::move(level); + } + else + { + level_from_nothing(); + } + if (reset) { m_tileset = TileManager::current()->get_tileset(m_level->get_tileset()); } @@ -657,7 +1039,7 @@ Editor::quit_editor() void Editor::check_unsaved_changes(const std::function& action) { - if (!m_levelloaded) + if (!m_levelloaded || m_temp_level) { action(); return; @@ -691,6 +1073,7 @@ Editor::check_unsaved_changes(const std::function& action) }); dialog->add_button(_("No"), [this, action] { action(); + set_level(nullptr, true); m_enabled = true; }); dialog->add_button(_("Cancel"), [this] { @@ -806,10 +1189,11 @@ Editor::setup() Tile::draw_editor_images = true; Sector::s_draw_solids_only = false; m_after_setup = true; - if (!m_levelloaded) { - + if (!m_levelloaded) + { #if 0 - if (AddonManager::current()->is_old_addon_enabled()) { + if (AddonManager::current()->is_old_addon_enabled()) + { auto dialog = std::make_unique(); dialog->set_text(_("Some obsolete add-ons are still active\nand might cause collisions with the default SuperTux structure.\nYou can still enable these add-ons in the menu.\nDisabling these add-ons will not delete your game progress.")); dialog->clear_buttons(); @@ -828,11 +1212,13 @@ Editor::setup() }); MenuManager::instance().set_dialog(std::move(dialog)); - } else + } + else #endif { MenuManager::instance().push_menu(MenuStorage::EDITOR_LEVELSET_SELECT_MENU); } + set_level(nullptr, true); } m_toolbox_widget->setup(); m_layers_widget->setup(); @@ -874,73 +1260,115 @@ Editor::on_window_resize() void Editor::event(const SDL_Event& ev) { - if (!m_enabled || !m_levelloaded) return; + if (!m_enabled || !m_levelloaded || + MenuManager::current()->is_active() || MenuManager::current()->has_dialog()) return; + + for(const auto& control : m_controls) + if (control->event(ev)) + return; try { - if (ev.type == SDL_KEYDOWN) + if (ev.type == SDL_MOUSEMOTION) { - m_ctrl_pressed = ev.key.keysym.mod & KMOD_CTRL; - - if (m_ctrl_pressed) - m_scroll_speed = 16.0f; - else if (ev.key.keysym.mod & KMOD_RSHIFT) - m_scroll_speed = 96.0f; + m_mouse_pos = VideoSystem::current()->get_viewport().to_logical(ev.motion.x, ev.motion.y); - if (ev.key.keysym.sym == SDLK_F6) + // If properties sidebar controls are active and the mouse is hovering over the sidebar, + // do not propagate mouse motion to the editor or its widgets. + if (!m_controls.empty() && Rectf(0, 32.0f, 200.0f, SCREEN_HEIGHT - 32.0f).contains(m_mouse_pos)) + return; + } + else + { + // If properties sidebar controls are active and the mouse is hovering over the sidebar, + // do not propagate mouse events to the editor or its widgets. + if (!m_controls.empty() && + (ev.type == SDL_MOUSEBUTTONDOWN || ev.type == SDL_MOUSEBUTTONUP || ev.type == SDL_MOUSEWHEEL) && + Rectf(0, 32.0f, 200.0f, SCREEN_HEIGHT - 32.0f).contains(m_mouse_pos)) { - Compositor::s_render_lighting = !Compositor::s_render_lighting; return; } - else if (m_ctrl_pressed) + + if (ev.type == SDL_KEYDOWN) { - switch (ev.key.keysym.sym) + m_ctrl_pressed = ev.key.keysym.mod & KMOD_CTRL; + m_shift_pressed = ev.key.keysym.mod & KMOD_SHIFT; + m_alt_pressed = ev.key.keysym.mod & KMOD_ALT; + + if (m_ctrl_pressed) + m_scroll_speed = 16.0f; + else if (ev.key.keysym.mod & KMOD_RSHIFT) + m_scroll_speed = 96.0f; + + if (ev.key.keysym.sym == SDLK_F6) + { + Compositor::s_render_lighting = !Compositor::s_render_lighting; + return; + } + else if (m_ctrl_pressed) { - case SDLK_t: - test_level(std::nullopt); - break; - case SDLK_s: - save_level(); - break; - case SDLK_z: - undo(); - break; - case SDLK_y: - redo(); - break; - case SDLK_PLUS: // Zoom in - case SDLK_KP_PLUS: - m_new_scale = m_sector->get_camera().get_current_scale() + CAMERA_ZOOM_SENSITIVITY; - break; - case SDLK_MINUS: // Zoom out - case SDLK_KP_MINUS: - m_new_scale = m_sector->get_camera().get_current_scale() - CAMERA_ZOOM_SENSITIVITY; - break; - case SDLK_d: // Reset zoom - m_new_scale = 1.f; - break; + switch (ev.key.keysym.sym) + { + case SDLK_t: + if (m_shift_pressed && m_alt_pressed) + { + test_level(m_last_test_pos); + break; + } + + if (m_shift_pressed) + m_last_test_pos = std::pair(get_sector()->get_name(), m_overlay_widget->get_sector_pos()); + else + m_last_test_pos = std::nullopt; + + test_level(m_last_test_pos); + break; + case SDLK_s: + save_level(); + break; + case SDLK_z: + undo(); + break; + case SDLK_y: + redo(); + break; + case SDLK_x: + toggle_tile_object_mode(); + break; + case SDLK_PLUS: // Zoom in + case SDLK_KP_PLUS: + m_new_scale = m_sector->get_camera().get_current_scale() + CAMERA_ZOOM_SENSITIVITY; + break; + case SDLK_MINUS: // Zoom out + case SDLK_KP_MINUS: + m_new_scale = m_sector->get_camera().get_current_scale() - CAMERA_ZOOM_SENSITIVITY; + break; + case SDLK_d: // Reset zoom + m_new_scale = 1.f; + break; + } } } - } - else if (ev.type == SDL_KEYUP) - { - m_ctrl_pressed = ev.key.keysym.mod & KMOD_CTRL; + else if (ev.type == SDL_KEYUP) + { + m_ctrl_pressed = ev.key.keysym.mod & KMOD_CTRL; + m_shift_pressed = ev.key.keysym.mod & KMOD_SHIFT; + m_alt_pressed = ev.key.keysym.mod & KMOD_ALT; - if (!m_ctrl_pressed && !(ev.key.keysym.mod & KMOD_RSHIFT)) - m_scroll_speed = 32.0f; - } - else if (ev.type == SDL_MOUSEMOTION) - { - m_mouse_pos = VideoSystem::current()->get_viewport().to_logical(ev.motion.x, ev.motion.y); - } - else if (ev.type == SDL_MOUSEWHEEL && !m_toolbox_widget->has_mouse_focus() && !m_layers_widget->has_mouse_focus()) - { - // Scroll or zoom with mouse wheel, if the mouse is not over the toolbox. - // The toolbox does scrolling independently from the main area. - if (m_ctrl_pressed) - m_new_scale = m_sector->get_camera().get_current_scale() + static_cast(ev.wheel.y) * CAMERA_ZOOM_SENSITIVITY; - else - scroll({ static_cast(ev.wheel.x * -32), static_cast(ev.wheel.y * -32) }); + if (!m_ctrl_pressed && !(ev.key.keysym.mod & KMOD_RSHIFT)) + m_scroll_speed = 32.0f; + } + else if (ev.type == SDL_MOUSEWHEEL && !m_toolbox_widget->has_mouse_focus() && !m_layers_widget->has_mouse_focus()) + { + // Scroll or zoom with mouse wheel, if the mouse is not over the toolbox. + // The toolbox does scrolling independently from the main area. + if (m_ctrl_pressed) + m_new_scale = m_sector->get_camera().get_current_scale() + static_cast(ev.wheel.y) * CAMERA_ZOOM_SENSITIVITY; + else if (m_shift_pressed) + scroll({ static_cast(ev.wheel.y * -40), static_cast(ev.wheel.x * -40) }); + else + scroll({ static_cast(ev.wheel.x * -40), static_cast(ev.wheel.y * -40) }); + } } BIND_SECTOR(*m_sector); @@ -954,6 +1382,49 @@ Editor::event(const SDL_Event& ev) } } +void +Editor::toggle_tile_object_mode() +{ + int i = 0, total = 0; + auto& tilebox = m_toolbox_widget->get_tilebox(); + const auto& input_type = tilebox.get_input_type(); + + if (input_type == InputType::OBJECT) + { + select_last_tilegroup(); + for(const auto& widget : m_widgets) + { + if (auto toolbar_button = dynamic_cast(widget.get())) + { + toolbar_button->set_visible(toolbar_button->get_visible_in_tile_mode()); + } + } + } + else + { + select_last_objectgroup(); + for(const auto& widget : m_widgets) + { + if (auto toolbar_button = dynamic_cast(widget.get())) + { + toolbar_button->set_visible(toolbar_button->get_visible_in_object_mode()); + } + } + } + + for (const auto& widget : m_widgets) + { + if (auto toolbar_button = dynamic_cast(widget.get())) + { + if (toolbar_button->get_visible()) + ++i; + } + } + + m_widgets_width = i * 32.f; +} + + void Editor::update_node_iterators() { @@ -978,6 +1449,12 @@ Editor::select_tilegroup(int id) m_toolbox_widget->select_tilegroup(id); } +void +Editor::select_last_tilegroup() +{ + m_toolbox_widget->select_last_tilegroup(); +} + const std::vector& Editor::get_tilegroups() const { @@ -1002,6 +1479,12 @@ Editor::select_objectgroup(int id) m_toolbox_widget->select_objectgroup(id); } +void +Editor::select_last_objectgroup() +{ + m_toolbox_widget->select_last_objectgroup(); +} + const std::vector& Editor::get_objectgroups() const { @@ -1058,33 +1541,8 @@ Editor::check_save_prerequisites(const std::function& callback) const void Editor::retoggle_undo_tracking() { - if (g_config->editor_undo_tracking && !m_undo_widget) - { - // Add undo/redo button widgets. - auto undo_button_widget = std::make_unique("images/engine/editor/undo.png", - Vector(10, 10), [this]{ undo(); }); - auto redo_button_widget = std::make_unique("images/engine/editor/redo.png", - Vector(60, 10), [this]{ redo(); }); - - m_undo_widget = undo_button_widget.get(); - m_redo_widget = redo_button_widget.get(); - - m_widgets.insert(m_widgets.begin(), std::move(undo_button_widget)); - m_widgets.insert(m_widgets.begin() + 1, std::move(redo_button_widget)); - } - else if (!g_config->editor_undo_tracking && m_undo_widget) - { - // Remove undo/redo button widgets. - m_widgets.erase(std::remove_if( - m_widgets.begin(), m_widgets.end(), - [this](const std::unique_ptr& widget) { - const Widget* ptr = widget.get(); - return ptr == m_undo_widget || ptr == m_redo_widget; - }), m_widgets.end()); - m_undo_widget = nullptr; - m_redo_widget = nullptr; - } - + m_undo_widget->set_disabled(true); + m_redo_widget->set_disabled(true); // Toggle undo tracking for all sectors. for (const auto& sector : m_level->m_sectors) sector->toggle_undo_tracking(g_config->editor_undo_tracking); @@ -1117,6 +1575,20 @@ Editor::redo() m_layers_widget->update_current_tip(); } +void +Editor::set_undo_disabled(bool state) +{ + if (m_undo_widget) + m_undo_widget->set_disabled(state); +} + +void +Editor::set_redo_disabled(bool state) +{ + if (m_redo_widget) + m_redo_widget->set_disabled(state); +} + IntegrationStatus Editor::get_status() const { @@ -1185,3 +1657,73 @@ Editor::pack_addon() *zip.Add_File(id + ".nfo") << ss.rdbuf(); } + +bool +Editor::get_properties_panel_visible() const +{ + return !m_controls.empty() && g_config->editor_show_properties_sidebar; +} + +void +Editor::add_control(const std::string& name, std::unique_ptr new_control, const std::string& description) +{ + assert(new_control); + if (!g_config->editor_show_properties_sidebar) + return; + + float height = 35.f; + for (const auto& control : m_controls) + height = std::max(height, control->get_rect().get_bottom() + 5.f); + + auto control_rect = new_control->get_rect(); + Rectf target_rect; + if (control_rect.get_width() == 0.f || control_rect.get_height() == 0.f) + { + target_rect = Rectf(100.f, height, 200.f - 1.0f, height + 20.f); + } + else + { + target_rect = Rectf(control_rect.get_left(), height, + control_rect.get_right(), height + control_rect.get_height()); + } + new_control->set_rect(target_rect); + + auto dimensions = Rectf(3.f, height, 100.f, height + 20.f); + new_control->m_label = std::make_unique(dimensions, std::move(name), std::move(description)); + m_controls.push_back(std::move(new_control)); +} + +void +Editor::select_object(GameObject* object) +{ + m_controls.clear(); + + if (!object || !g_config->editor_show_properties_sidebar) + { + m_selected_object = 0; + return; + } + m_selected_object = object; + + ObjectSettings os = object->get_settings(); + for (const auto& option : os.get_options()) + { + if ((option->get_flags() & OPTION_HIDDEN) && !(option->get_flags() & OPTION_VISIBLE_PROPERTIES)) + continue; + + auto control = option->create_interface_control(); + if (!control) + continue; + + control->m_on_activate_callbacks.emplace_back([object]() { + object->save_state(); + }); + control->m_on_change_callbacks.emplace_back([object]() { + // TODO: Updating the object doesn't work every time. + // Investigate why this is the case! + object->after_editor_set(); + object->check_state(); + }); + add_control(option->get_text(), std::move(control), option->get_description()); + } +} diff --git a/src/editor/editor.hpp b/src/editor/editor.hpp index 6bd4f173fc..795dfc04b8 100644 --- a/src/editor/editor.hpp +++ b/src/editor/editor.hpp @@ -27,15 +27,17 @@ #include "editor/toolbox_widget.hpp" #include "editor/layers_widget.hpp" #include "editor/scroller_widget.hpp" +#include "interface/control.hpp" #include "supertux/screen.hpp" #include "supertux/world.hpp" #include "util/currenton.hpp" #include "util/file_system.hpp" #include "util/log.hpp" +#include "util/script_manager.hpp" #include "util/string_util.hpp" #include "video/surface_ptr.hpp" -class ButtonWidget; +class EditorToolbarButtonWidget; class GameObject; class Level; class ObjectGroup; @@ -49,8 +51,13 @@ class Editor final : public Screen, public Currenton { public: + using exit_cb_t = std::function; + static bool is_active(); + static void may_deactivate(); + static void may_reactivate(); + private: static bool is_autosave_file(const std::string& filename) { return StringUtil::has_suffix(filename, "~"); @@ -90,6 +97,8 @@ class Editor final : public Screen, inline TileSet* get_tileset() const { return m_tileset; } inline TileSelection* get_tiles() const { return m_toolbox_widget->get_tilebox().get_tiles(); } inline std::string get_tileselect_object() const { return m_toolbox_widget->get_tilebox().get_object(); } + + void toggle_tile_object_mode(); inline EditorTilebox::InputType get_tileselect_input_type() const { return m_toolbox_widget->get_tilebox().get_input_type(); } @@ -99,7 +108,10 @@ class Editor final : public Screen, inline int get_tileselect_move_mode() const { return m_toolbox_widget->get_tileselect_move_mode(); } inline const std::string& get_levelfile() const { return m_levelfile; } + + void level_from_nothing(); + void set_level(std::unique_ptr level, bool reset = true); inline void set_level(const std::string& levelfile) { m_levelfile = levelfile; @@ -107,6 +119,8 @@ class Editor final : public Screen, } std::string get_level_directory() const; + + inline bool is_temp_level() const { return m_temp_level; } void open_level_directory(); @@ -135,12 +149,16 @@ class Editor final : public Screen, void esc_press(); void delete_markers(); void sort_layers(); + + inline void disable_testing() { m_testing_disabled = true; } void select_tilegroup(int id); + void select_last_tilegroup(); const std::vector& get_tilegroups() const; void change_tileset(); void select_objectgroup(int id); + void select_last_objectgroup(); const std::vector& get_objectgroups() const; void scroll(const Vector& velocity); @@ -161,17 +179,22 @@ class Editor final : public Screen, void queue_layers_refresh(); + bool get_properties_panel_visible() const; + void select_object(GameObject* object); + void retoggle_undo_tracking(); void undo_stack_cleanup(); void undo(); void redo(); + void set_undo_disabled(bool state); + void set_redo_disabled(bool state); void pack_addon(); + inline void on_exit(exit_cb_t exit_cb) { m_on_exit_cb = exit_cb; } private: void set_sector(Sector* sector); - void set_level(std::unique_ptr level, bool reset = true); void reload_level(); void reset_level(); void reactivate(); @@ -182,14 +205,15 @@ class Editor final : public Screen, * filename; subsequest saves will by default save to the * new filename. */ - void save_level(const std::string& filename = "", bool switch_file = false); + bool save_level(const std::string& filename = "", bool switch_file = false); void test_level(const std::optional>& test_pos); void update_keyboard(const Controller& controller); - void keep_camera_in_bounds(); + void add_control(const std::string& name, std::unique_ptr new_control, const std::string& description = ""); + protected: - std::unique_ptr m_level; + std::shared_ptr m_level; std::unique_ptr m_world; std::string m_levelfile; @@ -202,15 +226,25 @@ class Editor final : public Screen, bool m_reactivate_request; bool m_deactivate_request; bool m_save_request; + bool m_save_temp_level; std::string m_save_request_filename; bool m_save_request_switch; bool m_test_request; bool m_particle_editor_request; + bool m_testing_disabled; std::optional> m_test_pos; std::string* m_particle_editor_filename; bool m_ctrl_pressed; + bool m_shift_pressed; + bool m_alt_pressed; + + ScriptManager m_script_manager; + + exit_cb_t m_on_exit_cb; + + bool m_tilebox_something_selected; private: Sector* m_sector; @@ -221,14 +255,21 @@ class Editor final : public Screen, TileSet* m_tileset; bool m_has_deprecated_tiles; + bool m_temp_level; + std::optional> m_last_test_pos; std::vector > m_widgets; - ButtonWidget* m_undo_widget; - ButtonWidget* m_redo_widget; + std::vector> m_controls; + + EditorToolbarButtonWidget* m_undo_widget; + EditorToolbarButtonWidget* m_redo_widget; + EditorOverlayWidget* m_overlay_widget; EditorToolboxWidget* m_toolbox_widget; EditorLayersWidget* m_layers_widget; + TypedUID m_selected_object; + bool m_enabled; SurfacePtr m_bgr_surface; @@ -236,10 +277,15 @@ class Editor final : public Screen, float m_scroll_speed; float m_new_scale; + + float m_widgets_width; + float m_widgets_width_offset; Vector m_mouse_pos; bool m_layers_widget_needs_refresh; + + SpritePtr m_shadow; private: Editor(const Editor&) = delete; diff --git a/src/editor/editor_comment.cpp b/src/editor/editor_comment.cpp index eed18d9fd4..19c1855965 100644 --- a/src/editor/editor_comment.cpp +++ b/src/editor/editor_comment.cpp @@ -94,7 +94,7 @@ EditorComment::get_settings() { ObjectSettings result = MovingObject::get_settings(); - result.add_multiline_text(_("Comment"), &m_comment, "comment"); + result.add_multiline_text(get_uid(), _("Comment"), &m_comment, "comment"); return result; } diff --git a/src/editor/layers_widget.cpp b/src/editor/layers_widget.cpp index 600ade84a2..8e120a37c2 100644 --- a/src/editor/layers_widget.cpp +++ b/src/editor/layers_widget.cpp @@ -81,11 +81,13 @@ EditorLayersWidget::draw(DrawingContext& context) m_object_tip->draw_up(context, position); } + context.color().set_blur(g_config->editor_blur); context.color().draw_filled_rect(Rectf(Vector(0, static_cast(m_Ypos)), Vector(static_cast(m_Width), static_cast(SCREEN_HEIGHT))), g_config->editorcolor, 0.0f, LAYER_GUI-10); + context.color().set_blur(0); Rectf target_rect = Rectf(0, 0, 0, 0); bool draw_rect = true; @@ -128,6 +130,11 @@ EditorLayersWidget::draw(DrawingContext& context) Vector(35.0f, static_cast(m_Ypos) + 5.0f), ALIGN_LEFT, LAYER_GUI, ColorScheme::Menu::default_color); + // LAYERS_BOX_EXPERIMENT_BEGIN + context.color().draw_filled_rect(Rectf(Vector(0, SCREEN_HEIGHT - 400), Vector(200, SCREEN_HEIGHT)), + g_config->editorhovercolor, 0.0f, LAYER_GUI - 10); + // LAYERS_BOX_EXPERIMENT_END + int pos = 0; for (const auto& layer_icon : m_layer_icons) { @@ -137,6 +144,23 @@ EditorLayersWidget::draw(DrawingContext& context) layer_icon->draw(context, get_layer_coords(pos)); else if ((pos + 1) * 35 >= m_scroll) layer_icon->draw(context, get_layer_coords(pos), 35 - (m_scroll - pos * 35)); + + // LAYERS_BOX_EXPERIMENT_BEGIN + auto layer_icon_position = Vector(0, SCREEN_HEIGHT - 400 + (pos * 30)); + + layer_icon->draw(context, layer_icon_position); + + auto layer_name = layer_icon->get_layer()->get_name(); + if (layer_name.empty()) + { + layer_name = + fmt::format(fmt::runtime(_("Unnamed {}")), layer_icon->get_layer()->get_display_name()); + } + context.color().draw_text(Resources::small_font, layer_name, + layer_icon_position + Vector(35, 10), FontAlignment::ALIGN_LEFT, + LAYER_GUI - 9); + // LAYERS_BOX_EXPERIMENT_END + pos++; } if (pos * 35 >= m_scroll) @@ -218,10 +242,12 @@ EditorLayersWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) if (tilemap) { set_selected_tilemap(tilemap); m_editor.edit_path(tilemap->get_path_gameobject(), tilemap); + m_editor.select_object(tilemap); } else { auto cam = dynamic_cast(m_layer_icons[m_hovered_layer]->get_layer()); if (cam) { m_editor.edit_path(cam->get_path_gameobject(), cam); + m_editor.select_object(cam); } } } @@ -234,9 +260,8 @@ EditorLayersWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) else if (button.button == SDL_BUTTON_RIGHT) { if (m_hovered_item == HoveredItem::LAYERS && m_hovered_layer < m_layer_icons.size()) { - auto om = std::make_unique(m_layer_icons[m_hovered_layer]->get_layer()); - m_editor.m_deactivate_request = true; - MenuManager::instance().push_menu(std::move(om)); + MenuManager::instance().push_menu(std::make_unique(m_layer_icons[m_hovered_layer]->get_layer())); + m_editor.select_object(m_layer_icons[m_hovered_layer]->get_layer()); return true; } else { return false; diff --git a/src/editor/node_marker.cpp b/src/editor/node_marker.cpp index a55fa2f349..16eb537b62 100644 --- a/src/editor/node_marker.cpp +++ b/src/editor/node_marker.cpp @@ -119,7 +119,7 @@ NodeMarker::editor_delete() ObjectSettings NodeMarker::get_settings() { - ObjectSettings result(_("Path Node")); + ObjectSettings result(_("Path Node"), get_uid()); result.add_label(_("Press CTRL to move Bézier handles")); result.add_float(_("Time"), &(m_node->time)); result.add_float(_("Speed"), &(m_node->speed)); diff --git a/src/editor/object_menu.cpp b/src/editor/object_menu.cpp index 8b7b31d06d..7620c9a9cf 100644 --- a/src/editor/object_menu.cpp +++ b/src/editor/object_menu.cpp @@ -107,7 +107,6 @@ ObjectMenu::menu_action(MenuItem& item) case MNID_REMOVE: m_editor.delete_markers(); - m_editor.m_reactivate_request = true; m_object->remove_me(); MenuManager::instance().pop_menu(); break; @@ -147,7 +146,6 @@ ObjectMenu::on_back_action() if (!MenuManager::instance().previous_menu()) { - m_editor.m_reactivate_request = true; if (!dynamic_cast(m_object)) { m_editor.sort_layers(); } diff --git a/src/editor/object_option.cpp b/src/editor/object_option.cpp index 14873aa1ec..ca0bb535cd 100644 --- a/src/editor/object_option.cpp +++ b/src/editor/object_option.cpp @@ -22,11 +22,24 @@ #include #include +#include "editor/editor.hpp" #include "editor/object_menu.hpp" #include "gui/item_stringselect.hpp" #include "gui/menu.hpp" +#include "gui/menu_color.hpp" +#include "gui/menu_filesystem.hpp" +#include "gui/menu_list.hpp" #include "gui/menu_manager.hpp" #include "gui/menu_object_select.hpp" +#include "gui/menu_paths.hpp" +#include "gui/menu_script.hpp" +#include "gui/menu_string_array.hpp" +#include "interface/control_button.hpp" +#include "interface/control_checkbox.hpp" +#include "interface/control_enum.hpp" +#include "interface/control_textbox.hpp" +#include "interface/control_textbox_float.hpp" +#include "interface/control_textbox_int.hpp" #include "object/tilemap.hpp" #include "supertux/direction.hpp" #include "supertux/game_object_factory.hpp" @@ -54,12 +67,22 @@ bool BaseObjectOption::s_allow_saving_defaults = false; BaseObjectOption::BaseObjectOption(const std::string& text, const std::string& key, unsigned int flags) : m_text(text), + m_description(), m_key(key), m_flags(flags), m_last_state() { } +BaseObjectOption::BaseObjectOption(BaseObjectOption* other) : + m_text(other->m_text), + m_description(other->m_description), + m_key(other->m_key), + m_flags(other->m_flags), + m_last_state(other->m_last_state) +{ +} + std::string BaseObjectOption::save() const { @@ -129,6 +152,15 @@ BoolObjectOption::add_to_menu(Menu& menu) const menu.add_toggle(-1, get_text(), m_value_pointer); } +std::unique_ptr +BoolObjectOption::create_interface_control() const +{ + auto checkbox = std::make_unique(); + checkbox->set_rect(Rectf(140.f, 0.f, 160.f, 20.f)); + checkbox->bind_value(m_value_pointer); + return checkbox; +} + void BoolObjectOption::parse(const ReaderMapping& reader) { @@ -191,6 +223,15 @@ IntObjectOption::add_to_menu(Menu& menu) const menu.add_intfield(get_text(), m_value_pointer); } +std::unique_ptr +IntObjectOption::create_interface_control() const +{ + auto textbox = std::make_unique(); + textbox->set_rect(Rectf(0, 32, 200, 32)); + textbox->bind_value(m_value_pointer); + return textbox; +} + LabelObjectOption::LabelObjectOption(const std::string& text, unsigned int flags) : ObjectOption(text, "", flags) @@ -209,6 +250,12 @@ LabelObjectOption::add_to_menu(Menu& menu) const menu.add_label(m_text); } +std::unique_ptr +LabelObjectOption::create_interface_control() const +{ + return nullptr; +} + FloatObjectOption::FloatObjectOption(const std::string& text, float* pointer, const std::string& key, std::optional default_value, unsigned int flags) : @@ -247,6 +294,15 @@ FloatObjectOption::add_to_menu(Menu& menu) const menu.add_floatfield(get_text(), m_value_pointer); } +std::unique_ptr +FloatObjectOption::create_interface_control() const +{ + auto textbox = std::make_unique(); + textbox->set_rect(Rectf(0, 32, 200, 32)); + textbox->bind_value(m_value_pointer); + return textbox; +} + StringObjectOption::StringObjectOption(const std::string& text, std::string* pointer, const std::string& key, std::optional default_value, unsigned int flags) : @@ -287,11 +343,21 @@ StringObjectOption::add_to_menu(Menu& menu) const menu.add_textfield(get_text(), m_value_pointer); } -StringMultilineObjectOption::StringMultilineObjectOption(const std::string& text, std::string* pointer, const std::string& key, +std::unique_ptr +StringObjectOption::create_interface_control() const +{ + auto textbox = std::make_unique(); + textbox->set_rect(Rectf(0, 32, 200, 32)); + textbox->bind_string(m_value_pointer); + return textbox; +} + +StringMultilineObjectOption::StringMultilineObjectOption(UID uid, const std::string& text, std::string* pointer, const std::string& key, std::optional default_value, unsigned int flags) : ObjectOption(text, key, flags, pointer), - m_default_value(std::move(default_value)) + m_default_value(std::move(default_value)), + m_uid(uid) { } @@ -327,7 +393,18 @@ StringMultilineObjectOption::to_string() const void StringMultilineObjectOption::add_to_menu(Menu& menu) const { - menu.add_script(get_text(), m_value_pointer); + menu.add_script(m_uid, m_key, get_text(), m_value_pointer); +} + +std::unique_ptr +StringMultilineObjectOption::create_interface_control() const +{ + auto button = std::make_unique(_("Edit...")); + button->m_on_activate_callbacks.emplace_back([uid = m_uid, key = m_key, value_ptr = m_value_pointer]() { + MenuManager::instance().push_menu(std::make_unique(uid, key, value_ptr)); + }); + button->set_rect(Rectf(0, 32, 20, 32)); + return button; } StringSelectObjectOption::StringSelectObjectOption(const std::string& text, int* pointer, @@ -361,11 +438,11 @@ StringSelectObjectOption::save(Writer& writer) const std::string StringSelectObjectOption::to_string() const { - int* selected_id = static_cast(m_value_pointer); - if (*selected_id >= int(m_select.size()) || *selected_id < 0) { + int& selected_id = *m_value_pointer; + if (selected_id >= int(m_select.size()) || selected_id < 0) { return _("invalid"); //Test whether the selected ID is valid } else { - return m_select[*selected_id]; + return m_select[selected_id]; } } @@ -379,6 +456,23 @@ StringSelectObjectOption::add_to_menu(Menu& menu) const menu.add_string_select(-1, get_text(), m_value_pointer, m_select); } +std::unique_ptr +StringSelectObjectOption::create_interface_control() const +{ + auto dropdown = std::make_unique>(); + for (int i = 0; i < static_cast(m_select.size()); ++i) + { + dropdown->add_option(i, m_select[i]); + } + + int& selected_id = *m_value_pointer; + if (selected_id >= static_cast(m_select.size()) || selected_id < 0) + selected_id = 0; // Set the option to zero when not selectable + + dropdown->bind_value(m_value_pointer); + return dropdown; +} + EnumObjectOption::EnumObjectOption(const std::string& text, int* pointer, const std::vector& labels, const std::vector& symbols, @@ -439,10 +533,22 @@ EnumObjectOption::add_to_menu(Menu& menu) const menu.add_string_select(-1, get_text(), m_value_pointer, m_labels); } +std::unique_ptr +EnumObjectOption::create_interface_control() const +{ + auto dropdown = std::make_unique>(); + for (int i = 0; i < m_labels.size(); i++) + { + dropdown->add_option(i, m_labels[i]); + } + dropdown->bind_value(m_value_pointer); + return dropdown; +} -ScriptObjectOption::ScriptObjectOption(const std::string& text, std::string* pointer, const std::string& key, +ScriptObjectOption::ScriptObjectOption(UID uid, const std::string& text, std::string* pointer, const std::string& key, unsigned int flags) : - ObjectOption(text, key, flags, pointer) + ObjectOption(text, key, flags, pointer), + m_uid(uid) { } @@ -476,7 +582,18 @@ ScriptObjectOption::to_string() const void ScriptObjectOption::add_to_menu(Menu& menu) const { - menu.add_script(get_text(), m_value_pointer); + menu.add_script(m_uid, m_key, get_text(), m_value_pointer); +} + +std::unique_ptr +ScriptObjectOption::create_interface_control() const +{ + auto button = std::make_unique(_("Edit...")); + button->m_on_activate_callbacks.emplace_back([uid = m_uid, key = m_key, value_ptr = m_value_pointer]() { + MenuManager::instance().push_menu(std::make_unique(uid, key, value_ptr)); + }); + button->set_rect(Rectf(0, 32, 20, 32)); + return button; } FileObjectOption::FileObjectOption(const std::string& text, std::string* pointer, @@ -528,6 +645,19 @@ FileObjectOption::add_to_menu(Menu& menu) const menu.add_file(get_text(), m_value_pointer, m_filter, m_basedir, m_path_relative_to_basedir); } +std::unique_ptr +FileObjectOption::create_interface_control() const +{ + auto button = std::make_unique(_("Browse...")); + button->m_on_activate_callbacks.emplace_back( + [input = m_value_pointer, extensions = m_filter, basedir = m_basedir, path_relative_to_basedir = m_path_relative_to_basedir]() + { + MenuManager::instance().push_menu(std::make_unique(input, extensions, basedir, path_relative_to_basedir)); + }); + button->set_rect(Rectf(0, 32, 20, 32)); + return button; +} + ColorObjectOption::ColorObjectOption(const std::string& text, Color* pointer, const std::string& key, std::optional default_value, bool use_alpha, unsigned int flags) : @@ -573,6 +703,18 @@ ColorObjectOption::add_to_menu(Menu& menu) const menu.add_color(get_text(), m_value_pointer); } +std::unique_ptr +ColorObjectOption::create_interface_control() const +{ + auto button = std::make_unique(_("Mix...")); + button->m_on_activate_callbacks.emplace_back([color = m_value_pointer]() + { + MenuManager::instance().push_menu(std::make_unique(color)); + }); + button->set_rect(Rectf(0, 32, 20, 32)); + return button; +} + ObjectSelectObjectOption::ObjectSelectObjectOption(const std::string& text, std::vector>* pointer, uint8_t get_objects_param, const std::function)>& add_object_func, const std::string& key, unsigned int flags) : @@ -652,6 +794,18 @@ ObjectSelectObjectOption::add_to_menu(Menu& menu) const }); } +std::unique_ptr +ObjectSelectObjectOption::create_interface_control() const +{ + auto button = std::make_unique(_("Select...")); + button->m_on_activate_callbacks.emplace_back( + [pointer = m_value_pointer, get_objects_param = m_get_objects_param, add_object_func = m_add_object_function]() { + MenuManager::instance().push_menu(std::make_unique(*pointer, get_objects_param, add_object_func)); + }); + button->set_rect(Rectf(0, 32, 20, 32)); + return button; +} + TilesObjectOption::TilesState::TilesState() : width(), height(), @@ -689,6 +843,12 @@ TilesObjectOption::add_to_menu(Menu& menu) const { } +std::unique_ptr +TilesObjectOption::create_interface_control() const +{ + return nullptr; +} + void TilesObjectOption::save_state() { @@ -792,6 +952,12 @@ PathObjectOption::add_to_menu(Menu& menu) const { } +std::unique_ptr +PathObjectOption::create_interface_control() const +{ + return nullptr; +} + PathRefObjectOption::PathRefObjectOption(const std::string& text, PathObject& target, const std::string& path_ref, const std::string& key, unsigned int flags) : ObjectOption(text, key, flags, &target), @@ -825,6 +991,19 @@ PathRefObjectOption::add_to_menu(Menu& menu) const menu.add_path_settings(m_text, *m_value_pointer, m_path_ref); } +std::unique_ptr +PathRefObjectOption::create_interface_control() const +{ + auto button = std::make_unique(_("Change...")); + button->m_on_activate_callbacks.emplace_back( + [target_ptr = m_value_pointer, path_ref = m_path_ref]() { + MenuManager::instance().push_menu(std::make_unique(*target_ptr, path_ref)); + }); + button->set_rect(Rectf(0, 32, 20, 32)); + return button; + +} + SExpObjectOption::SExpObjectOption(const std::string& text, const std::string& key, sexp::Value& value, unsigned int flags) : ObjectOption(text, key, flags, &value) @@ -856,6 +1035,12 @@ SExpObjectOption::add_to_menu(Menu& menu) const { } +std::unique_ptr +SExpObjectOption::create_interface_control() const +{ + return nullptr; +} + PathHandleOption::PathHandleOption(const std::string& text, PathWalker::Handle& handle, const std::string& key, unsigned int flags) : ObjectOption(text, key, flags), @@ -906,6 +1091,12 @@ PathHandleOption::add_to_menu(Menu& menu) const menu.add_floatfield(get_text() + " (" + _("Offset Y") + ")", &m_target.m_pixel_offset.y); } +std::unique_ptr +PathHandleOption::create_interface_control() const +{ + return nullptr; +} + RemoveObjectOption::RemoveObjectOption() : ObjectOption(_("Remove"), "", 0) { @@ -923,8 +1114,14 @@ RemoveObjectOption::add_to_menu(Menu& menu) const menu.add_entry(ObjectMenu::MNID_REMOVE, get_text()); } -TestFromHereOption::TestFromHereOption() : - ObjectOption(_("Test from here"), "", 0) +std::unique_ptr +RemoveObjectOption::create_interface_control() const +{ + return nullptr; +} + +TestFromHereOption::TestFromHereOption(const MovingObject* object_ptr) : + ObjectOption(_("Test from here"), "", 0, object_ptr) { } @@ -940,6 +1137,22 @@ TestFromHereOption::add_to_menu(Menu& menu) const menu.add_entry(ObjectMenu::MNID_TEST_FROM_HERE, get_text()); } +std::unique_ptr +TestFromHereOption::create_interface_control() const +{ + auto button = std::make_unique(_("Test")); + button->set_rect(Rectf(0, 32, 200, 32)); + button->m_on_activate_callbacks.emplace_back([object_ptr = m_value_pointer]() { + Editor& editor = *Editor::current(); + // TODO: Pressing the return key from within a game session automatically + // triggers this button again if it's previously been pushed. This needs + // to get fixed. + editor.m_test_pos = std::make_pair(editor.get_sector()->get_name(), object_ptr->get_pos()); + editor.m_test_request = true; + }); + return button; +} + ParticleEditorOption::ParticleEditorOption() : ObjectOption(_("Open Particle Editor"), "", 0) { @@ -957,22 +1170,15 @@ ParticleEditorOption::add_to_menu(Menu& menu) const menu.add_entry(ObjectMenu::MNID_OPEN_PARTICLE_EDITOR, get_text()); } -ButtonOption::ButtonOption(const std::string& text, std::function callback) : - ObjectOption(text, "", 0), - m_callback(std::move(callback)) -{ -} - -std::string -ButtonOption::to_string() const -{ - return {}; -} - -void -ButtonOption::add_to_menu(Menu& menu) const +std::unique_ptr +ParticleEditorOption::create_interface_control() const { - menu.add_entry(get_text(), m_callback); + auto button = std::make_unique(_("Open")); + button->m_on_activate_callbacks.emplace_back([]() { + Editor::current()->m_particle_editor_request = true; + }); + button->set_rect(Rectf(0, 32, 20, 32)); + return button; } StringArrayOption::StringArrayOption(const std::string& text, const std::string& key, std::vector& items) : @@ -998,6 +1204,17 @@ StringArrayOption::add_to_menu(Menu& menu) const menu.add_string_array(get_text(), m_items); } +std::unique_ptr +StringArrayOption::create_interface_control() const +{ + auto button = std::make_unique(_("Change...")); + button->m_on_activate_callbacks.emplace_back([items_ptr = &m_items]() { + MenuManager::instance().push_menu(std::make_unique(*items_ptr)); + }); + button->set_rect(Rectf(0, 32, 20, 32)); + return button; +} + ListOption::ListOption(const std::string& text, const std::string& key, const std::vector& items, std::string* value_ptr) : ObjectOption(text, key, 0, value_ptr), m_items(items) @@ -1021,6 +1238,17 @@ ListOption::add_to_menu(Menu& menu) const menu.add_list(get_text(), m_items, m_value_pointer); } +std::unique_ptr +ListOption::create_interface_control() const +{ + auto button = std::make_unique(_("Change...")); + button->m_on_activate_callbacks.emplace_back([items = m_items, value_ptr = m_value_pointer]() { + MenuManager::instance().push_menu(std::make_unique(items, value_ptr, nullptr)); + }); + button->set_rect(Rectf(0, 32, 20, 32)); + return button; +} + DirectionOption::DirectionOption(const std::string& text, Direction* value_ptr, std::vector possible_directions, const std::string& key, unsigned int flags) : @@ -1074,3 +1302,15 @@ DirectionOption::add_to_menu(Menu& menu) const *value_ptr = possible_directions.at(index); }); } + +std::unique_ptr +DirectionOption::create_interface_control() const +{ + auto dropdown = std::make_unique>(); + for (const auto& direction : m_possible_directions) + { + dropdown->add_option(direction, dir_to_translated_string(direction)); + } + dropdown->bind_value(m_value_pointer); + return dropdown; +} diff --git a/src/editor/object_option.hpp b/src/editor/object_option.hpp index d103c42fa3..41efbceca6 100644 --- a/src/editor/object_option.hpp +++ b/src/editor/object_option.hpp @@ -31,8 +31,12 @@ enum ObjectOptionFlag { shouldn't be exposed to the user */ OPTION_HIDDEN = (1 << 0), + /** Set if the value should be hidden in ObjectMenu, but visible + on the properties sidebar */ + OPTION_VISIBLE_PROPERTIES = (1 << 1), + /** Set if the text should be saved as translatable */ - OPTION_TRANSLATABLE = (1 << 1) + OPTION_TRANSLATABLE = (1 << 2) }; namespace sexp { @@ -41,7 +45,9 @@ class Value; class Color; enum class Direction; class GameObject; +class InterfaceControl; class Menu; +class MovingObject; class Path; class PathObject; class ReaderMapping; @@ -56,12 +62,14 @@ class BaseObjectOption public: BaseObjectOption(const std::string& text, const std::string& key, unsigned int flags); + BaseObjectOption(BaseObjectOption* other); virtual ~BaseObjectOption() = default; virtual void parse(const ReaderMapping& reader) = 0; virtual void save(Writer& writer) const = 0; virtual std::string to_string() const = 0; virtual void add_to_menu(Menu& menu) const = 0; + virtual std::unique_ptr create_interface_control() const = 0; std::string save() const; @@ -73,10 +81,14 @@ class BaseObjectOption inline const std::string& get_key() const { return m_key; } inline const std::string& get_text() const { return m_text; } + inline const std::string& get_description() const { return m_description; } + void set_description(const std::string& description) { m_description = description; } + inline unsigned int get_flags() const { return m_flags; } protected: const std::string m_text; + std::string m_description; const std::string m_key; const unsigned int m_flags; @@ -115,6 +127,7 @@ class BoolObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: const std::optional m_default_value; @@ -135,6 +148,7 @@ class IntObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: const std::optional m_default_value; @@ -154,6 +168,7 @@ class LabelObjectOption final : public ObjectOption<> virtual void save(Writer& writer) const override {} virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: LabelObjectOption(const LabelObjectOption&) = delete; @@ -171,6 +186,7 @@ class FloatObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: const std::optional m_default_value; @@ -191,6 +207,7 @@ class StringObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: std::optional m_default_value; @@ -203,7 +220,7 @@ class StringObjectOption final : public ObjectOption class StringMultilineObjectOption final : public ObjectOption { public: - StringMultilineObjectOption(const std::string& text, std::string* pointer, const std::string& key, + StringMultilineObjectOption(UID uid, const std::string& text, std::string* pointer, const std::string& key, std::optional default_value, unsigned int flags); @@ -211,9 +228,11 @@ class StringMultilineObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: std::optional m_default_value; + UID m_uid; private: StringMultilineObjectOption(const StringMultilineObjectOption&) = delete; @@ -231,6 +250,7 @@ class StringSelectObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: const std::vector m_select; @@ -254,6 +274,11 @@ class EnumObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; + + const std::vector& get_labels() const { return m_labels; } + const std::vector& get_symbols() const { return m_symbols; } + const std::optional& get_default_value() const { return m_default_value; } private: const std::vector m_labels; @@ -268,13 +293,17 @@ class EnumObjectOption final : public ObjectOption class ScriptObjectOption final : public ObjectOption { public: - ScriptObjectOption(const std::string& text, std::string* pointer, const std::string& key, + ScriptObjectOption(UID uid, const std::string& text, std::string* pointer, const std::string& key, unsigned int flags); virtual void parse(const ReaderMapping& reader) override; virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; + +private: + UID m_uid; private: ScriptObjectOption(const ScriptObjectOption&) = delete; @@ -296,6 +325,7 @@ class FileObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: std::optional m_default_value; @@ -319,6 +349,7 @@ class ColorObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: const std::optional m_default_value; @@ -340,6 +371,7 @@ class ObjectSelectObjectOption final : public ObjectOption create_interface_control() const override; private: uint8_t m_get_objects_param; @@ -360,6 +392,7 @@ class TilesObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; virtual void save_state() override; virtual void parse_state(const ReaderMapping& reader) override; @@ -395,6 +428,7 @@ class PathObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: PathObjectOption(const PathObjectOption&) = delete; @@ -411,6 +445,7 @@ class PathRefObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: std::string m_path_ref; @@ -429,6 +464,7 @@ class SExpObjectOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: SExpObjectOption(const SExpObjectOption&) = delete; @@ -445,6 +481,7 @@ class PathHandleOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: PathWalker::Handle& m_target; @@ -463,21 +500,23 @@ class RemoveObjectOption final : public ObjectOption<> virtual void save(Writer& writer) const override {} virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: RemoveObjectOption(const RemoveObjectOption&) = delete; RemoveObjectOption& operator=(const RemoveObjectOption&) = delete; }; -class TestFromHereOption final : public ObjectOption<> +class TestFromHereOption final : public ObjectOption { public: - TestFromHereOption(); + TestFromHereOption(const MovingObject* object_ptr); virtual void parse(const ReaderMapping& reader) override {} virtual void save(Writer& writer) const override {} virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: TestFromHereOption(const TestFromHereOption&) = delete; @@ -493,30 +532,13 @@ class ParticleEditorOption final : public ObjectOption<> virtual void save(Writer& writer) const override {} virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: ParticleEditorOption(const ParticleEditorOption&) = delete; ParticleEditorOption& operator=(const ParticleEditorOption&) = delete; }; -class ButtonOption final : public ObjectOption<> -{ -public: - ButtonOption(const std::string& text, std::function callback); - - virtual void parse(const ReaderMapping& reader) override {} - virtual void save(Writer& writer) const override {} - virtual std::string to_string() const override; - virtual void add_to_menu(Menu& menu) const override; - -private: - std::function m_callback; - -private: - ButtonOption(const ButtonOption&) = delete; - ButtonOption& operator=(const ButtonOption&) = delete; -}; - class StringArrayOption final : public ObjectOption<> { public: @@ -526,6 +548,7 @@ class StringArrayOption final : public ObjectOption<> virtual void save(Writer& writer) const override; virtual std::string to_string() const override { return "text-area"; } virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: std::vector& m_items; @@ -544,6 +567,7 @@ class ListOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override { return *m_value_pointer; } virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; private: const std::vector& m_items; @@ -564,6 +588,12 @@ class DirectionOption final : public ObjectOption virtual void save(Writer& writer) const override; virtual std::string to_string() const override; virtual void add_to_menu(Menu& menu) const override; + virtual std::unique_ptr create_interface_control() const override; + + const std::vector& get_possible_directions() const + { + return m_possible_directions; + } private: std::vector m_possible_directions; diff --git a/src/editor/object_settings.cpp b/src/editor/object_settings.cpp index 3215d3fea0..832d39a684 100644 --- a/src/editor/object_settings.cpp +++ b/src/editor/object_settings.cpp @@ -19,23 +19,38 @@ #include #include +#include "editor/editor.hpp" +#include "editor/object_option.hpp" #include "util/gettext.hpp" #include "util/log.hpp" #include "video/color.hpp" -ObjectSettings::ObjectSettings(const std::string& name) : - m_name(name), +ObjectSettings::ObjectSettings(std::string name, UID uid) : + m_name(std::move(name)), + m_uid(std::move(uid)), m_options() { } ObjectSettings::ObjectSettings(ObjectSettings&& other) : m_name(other.m_name), + m_uid(other.m_uid), m_options(std::move(other.m_options)) { } -void +ObjectSettings::ObjectSettings(ObjectSettings* obj) : + m_name(obj->m_name), + m_uid(obj->m_uid), + m_options() +{ + // for (auto &option : obj->m_options) + // { + // m_options.emplace_back(std::make_unique(option.get())); + // } +} + +std::unique_ptr& ObjectSettings::add_option(std::unique_ptr option) { if (!option->get_key().empty()) @@ -49,179 +64,181 @@ ObjectSettings::add_option(std::unique_ptr option) } m_options.push_back(std::move(option)); + + return m_options.back(); } -void +std::unique_ptr& ObjectSettings::add_objects(const std::string& text, std::vector>* value_ptr, uint8_t get_objects_param, const std::function)>& add_object_func, const std::string& key, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, get_objects_param, add_object_func, key, flags)); + return add_option(std::make_unique(text, value_ptr, get_objects_param, add_object_func, key, flags)); } -void +std::unique_ptr& ObjectSettings::add_color(const std::string& text, Color* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, true, flags)); + return add_option(std::make_unique(text, value_ptr, key, default_value, true, flags)); } -void +std::unique_ptr& ObjectSettings::add_rgba(const std::string& text, Color* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, true, flags)); + return add_option(std::make_unique(text, value_ptr, key, default_value, true, flags)); } -void +std::unique_ptr& ObjectSettings::add_rgb(const std::string& text, Color* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, false, flags)); + return add_option(std::make_unique(text, value_ptr, key, default_value, false, flags)); } -void +std::unique_ptr& ObjectSettings::add_bool(const std::string& text, bool* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, flags)); + return add_option(std::make_unique(text, value_ptr, key, default_value, flags)); } -void +std::unique_ptr& ObjectSettings::add_float(const std::string& text, float* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, flags)); + return add_option(std::make_unique(text, value_ptr, key, default_value, flags)); } -void +std::unique_ptr& ObjectSettings::add_int(const std::string& text, int* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, flags)); + return add_option(std::make_unique(text, value_ptr, key, default_value, flags)); } -void +std::unique_ptr& ObjectSettings::add_label(const std::string& text, unsigned int flags) { - add_option(std::make_unique(text, flags)); + return add_option(std::make_unique(text, flags)); } -void +std::unique_ptr& ObjectSettings::add_direction(const std::string& text, Direction* value_ptr, std::vector possible_directions, const std::string& key, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, std::move(possible_directions), - key, flags)); + return add_option(std::make_unique(text, value_ptr, std::move(possible_directions), + key, flags)); } -void +std::unique_ptr& ObjectSettings::add_worldmap_direction(const std::string& text, worldmap::Direction* value_ptr, std::optional default_value, const std::string& key, unsigned int flags) { - add_enum(text, reinterpret_cast(value_ptr), + return add_enum(text, reinterpret_cast(value_ptr), {_("None"), _("West"), _("East"), _("North"), _("South")}, {"none", "west", "east", "north", "south"}, default_value ? static_cast(*default_value) : std::optional(), key, flags); } -void +std::unique_ptr& ObjectSettings::add_walk_mode(const std::string& text, WalkMode* value_ptr, const std::optional& default_value, const std::string& key, unsigned int flags) { - add_option(std::make_unique( + return add_option(std::make_unique( text, reinterpret_cast(value_ptr), std::vector{_("One shot"), _("Ping-pong"), _("Circular")}, std::nullopt, key, flags)); } -void +std::unique_ptr& ObjectSettings::add_remove() { - add_option(std::make_unique()); + return add_option(std::make_unique()); } -void -ObjectSettings::add_script(const std::string& text, std::string* value_ptr, +std::unique_ptr& +ObjectSettings::add_script(UID uid, const std::string& text, std::string* value_ptr, const std::string& key, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, flags)); + return add_option(std::make_unique(uid, text, value_ptr, key, flags)); } -void +std::unique_ptr& ObjectSettings::add_text(const std::string& text, std::string* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, flags)); + return add_option(std::make_unique(text, value_ptr, key, default_value, flags)); } -void +std::unique_ptr& ObjectSettings::add_translatable_text(const std::string& text, std::string* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, + return add_option(std::make_unique(text, value_ptr, key, default_value, flags | OPTION_TRANSLATABLE)); } -void -ObjectSettings::add_multiline_text(const std::string& text, std::string* value_ptr, +std::unique_ptr& +ObjectSettings::add_multiline_text(UID uid, const std::string& text, std::string* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, flags)); + return add_option(std::make_unique(uid, text, value_ptr, key, default_value, flags)); } -void -ObjectSettings::add_multiline_translatable_text(const std::string& text, std::string* value_ptr, +std::unique_ptr& +ObjectSettings::add_multiline_translatable_text(UID uid, const std::string& text, std::string* value_ptr, const std::string& key, const std::optional& default_value, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, default_value, + return add_option(std::make_unique(uid, text, value_ptr, key, default_value, flags | OPTION_TRANSLATABLE)); } -void +std::unique_ptr& ObjectSettings::add_string_select(const std::string& text, int* value_ptr, const std::vector& select, const std::optional& default_value, const std::string& key, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, select, default_value, key, flags)); + return add_option(std::make_unique(text, value_ptr, select, default_value, key, flags)); } -void +std::unique_ptr& ObjectSettings::add_enum(const std::string& text, int* value_ptr, const std::vector& labels, const std::vector& symbols, const std::optional& default_value, const std::string& key, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, labels, symbols, default_value, key, flags)); + return add_option(std::make_unique(text, value_ptr, labels, symbols, default_value, key, flags)); } -void +std::unique_ptr& ObjectSettings::add_file(const std::string& text, std::string* value_ptr, const std::string& key, const std::optional& default_value, @@ -230,29 +247,27 @@ ObjectSettings::add_file(const std::string& text, std::string* value_ptr, bool path_relative_to_basedir, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, default_value, key, filter, basedir, path_relative_to_basedir, flags)); + return add_option(std::make_unique(text, value_ptr, default_value, key, filter, basedir, path_relative_to_basedir, flags)); } -void +std::unique_ptr& ObjectSettings::add_tiles(const std::string& text, TileMap* value_ptr, const std::string& key, unsigned int flags) { - add_option(std::make_unique(text, value_ptr, key, flags)); + return add_option(std::make_unique(text, value_ptr, key, flags)); } -void +std::unique_ptr& ObjectSettings::add_path(const std::string& text, Path* path, const std::string& key, unsigned int flags) { - add_option(std::make_unique(text, path, key, flags)); + return add_option(std::make_unique(text, path, key, flags)); } -void +std::unique_ptr& ObjectSettings::add_path_ref(const std::string& text, PathObject& target, const std::string& path_ref, const std::string& key, unsigned int flags) { - add_option(std::make_unique(text, target, path_ref, key, flags)); - if (!path_ref.empty()) { m_options.erase(std::remove_if(m_options.begin(), m_options.end(), [](const std::unique_ptr& obj) { @@ -260,103 +275,98 @@ ObjectSettings::add_path_ref(const std::string& text, PathObject& target, const }), m_options.end()); } + return add_option(std::make_unique(text, target, path_ref, key, flags)); } -void +std::unique_ptr& ObjectSettings::add_level(const std::string& text, std::string* value_ptr, const std::string& key, const std::string& basedir, unsigned int flags) { - add_file(text, value_ptr, key, {}, {".stl"}, basedir, true, flags); + return add_file(text, value_ptr, key, {}, {".stl"}, basedir, true, flags); } -void +std::unique_ptr& ObjectSettings::add_sprite(const std::string& text, std::string* value_ptr, const std::string& key, std::optional default_value, unsigned int flags) { - add_file(text, value_ptr, key, std::move(default_value), {".jpg", ".png", ".sprite"}, {}, true, flags); + return add_file(text, value_ptr, key, std::move(default_value), {".jpg", ".png", ".sprite"}, {}, true, flags); } -void +std::unique_ptr& ObjectSettings::add_surface(const std::string& text, std::string* value_ptr, const std::string& key, std::optional default_value, unsigned int flags) { - add_file(text, value_ptr, key, std::move(default_value), {".jpg", ".png", ".surface"}, {}, true, flags); + return add_file(text, value_ptr, key, std::move(default_value), {".jpg", ".png", ".surface"}, {}, true, flags); } -void +std::unique_ptr& ObjectSettings::add_sound(const std::string& text, std::string* value_ptr, const std::string& key, std::optional default_value, unsigned int flags) { - add_file(text, value_ptr, key, std::move(default_value), {".wav", ".ogg"}, {}, true, flags); + return add_file(text, value_ptr, key, std::move(default_value), {".wav", ".ogg"}, {}, true, flags); } -void +std::unique_ptr& ObjectSettings::add_music(const std::string& text, std::string* value_ptr, const std::string& key, std::optional default_value, unsigned int flags) { - add_file(text, value_ptr, key, std::move(default_value), {".music"}, {"/music"}, false, flags); + return add_file(text, value_ptr, key, std::move(default_value), {".music"}, {"/music"}, false, flags); } -void +std::unique_ptr& ObjectSettings::add_worldmap(const std::string& text, std::string* value_ptr, const std::string& key, unsigned int flags) { - add_file(text, value_ptr, key, {}, {".stwm"}, {}, true, flags); + return add_file(text, value_ptr, key, {}, {".stwm"}, {}, true, flags); } -void +std::unique_ptr& ObjectSettings::add_sexp(const std::string& text, const std::string& key, sexp::Value& value, unsigned int flags) { - add_option(std::make_unique(text, key, value, flags)); + return add_option(std::make_unique(text, key, value, flags)); } -void +std::unique_ptr& ObjectSettings::add_string_array(const std::string& text, const std::string& key, std::vector& items) { - add_option(std::make_unique(text, key, items)); + return add_option(std::make_unique(text, key, items)); } -void -ObjectSettings::add_test_from_here() +std::unique_ptr& +ObjectSettings::add_test_from_here(const MovingObject* object_ptr) { - add_option(std::make_unique()); + return add_option(std::make_unique(object_ptr)); } -void +std::unique_ptr& ObjectSettings::add_particle_editor() { - add_option(std::make_unique()); + return add_option(std::make_unique()); } -void +std::unique_ptr& ObjectSettings::add_path_handle(const std::string& text, PathWalker::Handle& handle, const std::string& key, unsigned int flags) { - add_option(std::make_unique(text, handle, key, flags)); + return add_option(std::make_unique(text, handle, key, flags)); } -void -ObjectSettings::add_button(const std::string& text, const std::function& callback) -{ - add_option(std::make_unique(text, callback)); -} - -void +std::unique_ptr& ObjectSettings::add_list(const std::string& text, const std::string& key, const std::vector& items, std::string* value_ptr) { - add_option(std::make_unique(text, key, items, value_ptr)); + return add_option(std::make_unique(text, key, items, value_ptr)); } void diff --git a/src/editor/object_settings.hpp b/src/editor/object_settings.hpp index 5ada65f829..5b85c75785 100644 --- a/src/editor/object_settings.hpp +++ b/src/editor/object_settings.hpp @@ -26,6 +26,7 @@ class Color; enum class Direction; class GameObject; +class MovingObject; class PathObject; class ReaderMapping; enum class WalkMode; @@ -42,123 +43,121 @@ class Value; class ObjectSettings final { public: - ObjectSettings(const std::string& name); + ObjectSettings(std::string name, UID uid); ObjectSettings(ObjectSettings&& other); + ObjectSettings(ObjectSettings* obj); ObjectSettings& operator=(ObjectSettings&&) = default; inline const std::string& get_name() const { return m_name; } - void add_bool(const std::string& text, bool* value_ptr, + std::unique_ptr& add_bool(const std::string& text, bool* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_float(const std::string& text, float* value_ptr, + std::unique_ptr& add_float(const std::string& text, float* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_int(const std::string& text, int* value_ptr, + std::unique_ptr& add_int(const std::string& text, int* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_label(const std::string& text, unsigned int flags = 0); - void add_worldmap_direction(const std::string& text, worldmap::Direction* value_ptr, + std::unique_ptr& add_label(const std::string& text, unsigned int flags = 0); + std::unique_ptr& add_worldmap_direction(const std::string& text, worldmap::Direction* value_ptr, std::optional default_value = {}, const std::string& key = {}, unsigned int flags = 0); - void add_direction(const std::string& text, Direction* value_ptr, + std::unique_ptr& add_direction(const std::string& text, Direction* value_ptr, std::vector possible_directions = {}, const std::string& key = {}, unsigned int flags = 0); - void add_walk_mode(const std::string& text, WalkMode* value_ptr, + std::unique_ptr& add_walk_mode(const std::string& text, WalkMode* value_ptr, const std::optional& default_value = {}, const std::string& key = {}, unsigned int flags = 0); - void add_objects(const std::string& text, std::vector>* value_ptr, + std::unique_ptr& add_objects(const std::string& text, std::vector>* value_ptr, uint8_t get_objects_param = 0, const std::function)>& add_object_func = {}, const std::string& key = {}, unsigned int flags = 0); - void add_color(const std::string& text, Color* value_ptr, + std::unique_ptr& add_color(const std::string& text, Color* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_rgba(const std::string& text, Color* value_ptr, + std::unique_ptr& add_rgba(const std::string& text, Color* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_rgb(const std::string& text, Color* value_ptr, + std::unique_ptr& add_rgb(const std::string& text, Color* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_remove(); - void add_script(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_remove(); + std::unique_ptr& add_script(UID uid, const std::string& text, std::string* value_ptr, const std::string& key = {}, unsigned int flags = 0); - void add_text(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_text(const std::string& text, std::string* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_translatable_text(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_translatable_text(const std::string& text, std::string* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_multiline_text(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_multiline_text(UID uid, const std::string& text, std::string* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_multiline_translatable_text(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_multiline_translatable_text(UID uid, const std::string& text, std::string* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, unsigned int flags = 0); - void add_string_select(const std::string& text, int* value_ptr, const std::vector& select, + std::unique_ptr& add_string_select(const std::string& text, int* value_ptr, const std::vector& select, const std::optional& default_value = {}, const std::string& key = {}, unsigned int flags = 0); - void add_enum(const std::string& text, int* value_ptr, + std::unique_ptr& add_enum(const std::string& text, int* value_ptr, const std::vector& labels, const std::vector& symbols, const std::optional& default_value = {}, const std::string& key = {}, unsigned int flags = 0); - void add_sprite(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_sprite(const std::string& text, std::string* value_ptr, const std::string& key = {}, std::optional default_value = {}, unsigned int flags = 0); - void add_surface(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_surface(const std::string& text, std::string* value_ptr, const std::string& key = {}, std::optional default_value = {}, unsigned int flags = 0); - void add_sound(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_sound(const std::string& text, std::string* value_ptr, const std::string& key = {}, std::optional default_value = {}, unsigned int flags = 0); - void add_music(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_music(const std::string& text, std::string* value_ptr, const std::string& key = {}, std::optional default_value = {}, unsigned int flags = 0); - void add_worldmap(const std::string& text, std::string* value_ptr, const std::string& key = {}, + std::unique_ptr& add_worldmap(const std::string& text, std::string* value_ptr, const std::string& key = {}, unsigned int flags = 0); - void add_level(const std::string& text, std::string* value_ptr, const std::string& basedir, + std::unique_ptr& add_level(const std::string& text, std::string* value_ptr, const std::string& basedir, const std::string& key = {}, unsigned int flags = 0); - void add_tiles(const std::string& text, TileMap* value_ptr, const std::string& key = {}, + std::unique_ptr& add_tiles(const std::string& text, TileMap* value_ptr, const std::string& key = {}, unsigned int flags = 0); - void add_path(const std::string& text, Path* path, const std::string& key = {}, + std::unique_ptr& add_path(const std::string& text, Path* path, const std::string& key = {}, unsigned int flags = 0); - void add_path_ref(const std::string& text, PathObject& target, const std::string& path_ref, + std::unique_ptr& add_path_ref(const std::string& text, PathObject& target, const std::string& path_ref, const std::string& key = {}, unsigned int flags = 0); - void add_file(const std::string& text, std::string* value_ptr, + std::unique_ptr& add_file(const std::string& text, std::string* value_ptr, const std::string& key = {}, const std::optional& default_value = {}, const std::vector& filter = {}, const std::string& basedir = {}, bool path_relative_to_basedir = true, unsigned int flags = 0); - void add_sexp(const std::string& text, const std::string& key, + std::unique_ptr& add_sexp(const std::string& text, const std::string& key, sexp::Value& value, unsigned int flags = 0); - void add_string_array(const std::string& text, const std::string& key, std::vector& items); - void add_test_from_here(); - void add_particle_editor(); - void add_path_handle(const std::string& text, PathWalker::Handle& handle, + std::unique_ptr& add_string_array(const std::string& text, const std::string& key, std::vector& items); + std::unique_ptr& add_test_from_here(const MovingObject* object_ptr); + std::unique_ptr& add_particle_editor(); + std::unique_ptr& add_path_handle(const std::string& text, PathWalker::Handle& handle, const std::string& key = {}, unsigned int flags = 0); - void add_list(const std::string& text, const std::string& key, const std::vector& items, std::string* value_ptr); - - // VERY UNSTABLE - use with care ~ Semphris (author of that option) - void add_button(const std::string& text, const std::function& callback); + std::unique_ptr& add_list(const std::string& text, const std::string& key, const std::vector& items, std::string* value_ptr); inline const std::vector>& get_options() const { return m_options; } @@ -186,10 +185,11 @@ class ObjectSettings final void save_new_state(Writer& writer) const; private: - void add_option(std::unique_ptr option); + std::unique_ptr& add_option(std::unique_ptr option); private: std::string m_name; + UID m_uid; std::vector > m_options; private: diff --git a/src/editor/overlay_widget.cpp b/src/editor/overlay_widget.cpp index a845833127..7e13682104 100644 --- a/src/editor/overlay_widget.cpp +++ b/src/editor/overlay_widget.cpp @@ -22,13 +22,16 @@ #include "editor/node_marker.hpp" #include "editor/object_menu.hpp" #include "editor/object_info.hpp" +#include "editor/object_option.hpp" #include "editor/tile_selection.hpp" #include "editor/tip.hpp" #include "gui/menu.hpp" #include "gui/menu_manager.hpp" +#include "interface/control.hpp" #include "math/bezier.hpp" #include "object/camera.hpp" #include "object/path_gameobject.hpp" +#include "object/spawnpoint.hpp" #include "object/tilemap.hpp" #include "supertux/gameconfig.hpp" #include "supertux/autotile.hpp" @@ -65,6 +68,7 @@ EditorOverlayWidget::EditorOverlayWidget(Editor& editor) : m_hovered_tile(0, 0), m_hovered_tile_prev(0, 0), m_last_hovered_tile(0, 0), + m_last_target_pos(0, 0), m_sector_pos(0, 0), m_mouse_pos(0, 0), m_previous_mouse_pos(0, 0), @@ -203,6 +207,13 @@ EditorOverlayWidget::put_tiles(const Vector& target_tile, TileSelection* tiles) { m_editor.get_selected_tilemap()->save_state(); + // Don't put tile if the position (or tile) hasn't changed + if (floor(m_last_target_pos.x) == floor(target_tile.x) && + floor(m_last_target_pos.y) == floor(target_tile.y) && + Editor::current()->m_tilebox_something_selected == false) + { + return; + } Vector add_tile(0.0f, 0.0f); for (add_tile.x = static_cast(tiles->m_width) - 1.0f; add_tile.x >= 0.0f; add_tile.x--) { @@ -226,6 +237,9 @@ EditorOverlayWidget::put_tiles(const Vector& target_tile, TileSelection* tiles) input_tile(target_tile + add_tile, tile); } // for tile y } // for tile x + + m_last_target_pos = target_tile; + Editor::current()->m_tilebox_something_selected = false; } namespace { @@ -615,6 +629,7 @@ EditorOverlayWidget::grab_object() if (!m_hovered_object->is_valid()) { m_hovered_object = nullptr; + m_editor.select_object(nullptr); } else { @@ -644,6 +659,8 @@ EditorOverlayWidget::grab_object() { delete_markers(); } + + m_editor.select_object(nullptr); } } @@ -669,6 +686,13 @@ EditorOverlayWidget::clone_object() if (path_object) path_object->editor_clone_path(dynamic_cast(m_hovered_object.get())->get_path_gameobject()); + if (auto moving_object = dynamic_cast(obj.get())) + { + // Move cloned objects half a tile down so the user gets + // a visual feedback that the object was cloned + moving_object->move(Vector(16, 16)); + } + m_dragged_object = static_cast(&m_editor.get_sector()->add_object(std::move(obj))); m_dragged_object->after_editor_set(); } @@ -682,9 +706,7 @@ EditorOverlayWidget::clone_object() void EditorOverlayWidget::show_object_menu(GameObject& object) { - auto menu = std::make_unique(&object); - m_editor.m_deactivate_request = true; - MenuManager::instance().push_menu(std::move(menu)); + MenuManager::instance().push_menu(std::make_unique(&object)); } void @@ -701,7 +723,7 @@ EditorOverlayWidget::move_object() if (g_config->editor_snap_to_grid) { auto& snap_grid_size = snap_grid_sizes[g_config->editor_selected_snap_grid_size]; - new_pos = glm::floor(new_pos / static_cast(snap_grid_size)) * static_cast(snap_grid_size); + new_pos = glm::round(new_pos / static_cast(snap_grid_size)) * static_cast(snap_grid_size); auto pm = dynamic_cast(m_dragged_object.get()); if (pm) @@ -729,6 +751,7 @@ EditorOverlayWidget::move_object() void EditorOverlayWidget::rubber_object() { + m_editor.select_object(nullptr); if (!m_edited_path) { delete_markers(); } @@ -816,7 +839,7 @@ EditorOverlayWidget::put_object() if (g_config->editor_snap_to_grid) { auto& snap_grid_size = snap_grid_sizes[g_config->editor_selected_snap_grid_size]; - target_pos = glm::floor(m_sector_pos / static_cast(snap_grid_size)) * static_cast(snap_grid_size); + target_pos = glm::floor(target_pos / static_cast(snap_grid_size)) * static_cast(snap_grid_size); } auto object = GameObjectFactory::instance().create(object_class, target_pos); @@ -878,6 +901,9 @@ EditorOverlayWidget::process_left_click() case EditorTilebox::InputType::NONE: case EditorTilebox::InputType::OBJECT: + if (m_hovered_object) + m_editor.select_object(m_hovered_object.get()); + switch (m_editor.get_tileselect_move_mode()) { case 0: @@ -1134,11 +1160,7 @@ EditorOverlayWidget::on_key_up(const SDL_KeyboardEvent& key) { std::uint16_t mod = key.keysym.mod; - if (mod & KMOD_SHIFT) - { - g_config->editor_snap_to_grid = !g_config->editor_snap_to_grid; - } - else if (!m_editor.m_ctrl_pressed) + if (!m_editor.m_ctrl_pressed) { m_autotile_mode = g_config->editor_autotile_mode; @@ -1162,7 +1184,7 @@ EditorOverlayWidget::on_key_down(const SDL_KeyboardEvent& key) { g_config->editor_render_grid = !g_config->editor_render_grid; } - else if (sym == SDLK_F7 || mod & KMOD_SHIFT) + else if (sym == SDLK_F7) { g_config->editor_snap_to_grid = !g_config->editor_snap_to_grid; } @@ -1209,6 +1231,7 @@ EditorOverlayWidget::update_pos() m_sector_pos = m_mouse_pos / m_editor.get_sector()->get_camera().get_current_scale() + m_editor.get_sector()->get_camera().get_translation(); + m_hovered_tile = sp_to_tp(m_sector_pos); if (m_last_hovered_tile != m_hovered_tile) @@ -1414,8 +1437,6 @@ EditorOverlayWidget::draw_tile_grid(DrawingContext& context, int tile_size, bool void EditorOverlayWidget::draw_tilemap_border(DrawingContext& context) { - if (!m_editor.get_selected_tilemap()) return; - auto current_tm = m_editor.get_selected_tilemap(); if (!current_tm) return; @@ -1428,6 +1449,34 @@ EditorOverlayWidget::draw_tilemap_border(DrawingContext& context) context.color().draw_line(Vector(end.x, start.y), end, Color(1, 0, 1), current_tm->get_layer()); } +void +EditorOverlayWidget::draw_tilemap_outer_shading(DrawingContext& context) +{ + auto current_tm = m_editor.get_selected_tilemap(); + if (!current_tm) return; + + Vector start = tile_screen_pos( Vector(0, 0) ); + Vector end = tile_screen_pos( Vector(static_cast(current_tm->get_width()), + static_cast(current_tm->get_height())) ); + + const Color& bg_color = { 0, 0, 0, 0.15 }; + const Camera& camera = m_editor.get_sector()->get_camera(); + float w_l = (-camera.get_x()) * camera.get_current_scale(); + float height = camera.get_screen_height(); + float w_r = (current_tm->get_width() - camera.get_x()) * camera.get_current_scale(); + // Left + context.color().draw_filled_rect({0,0,start.x,height}, bg_color, current_tm->get_layer()); + // Top + context.color().draw_filled_rect({start.x, 0, end.x, start.y}, bg_color, current_tm->get_layer()); + // Right + context.color().draw_filled_rect( + {end.x, 0, + static_cast(context.get_viewport().get_right()), height}, + bg_color, current_tm->get_layer()); + // Bottom + context.color().draw_filled_rect({start.x, end.y, end.x, height}, bg_color, current_tm->get_layer()); +} + void EditorOverlayWidget::draw_path(DrawingContext& context) { @@ -1490,7 +1539,6 @@ EditorOverlayWidget::draw(DrawingContext& context) if (g_config->editor_render_grid) { draw_tile_grid(context, 32, true); - draw_tilemap_border(context); auto snap_grid_size = snap_grid_sizes[g_config->editor_selected_snap_grid_size]; if (snap_grid_size != 32) { @@ -1576,6 +1624,12 @@ EditorOverlayWidget::draw(DrawingContext& context) context.color().draw_text(Resources::normal_font, m_warning_text, Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::warning_color); } + Vector hint_pos(32, 16); + if (g_config->editor_show_toolbar_widgets || m_editor.get_properties_panel_visible()) + hint_pos.y += 32.f; + // TODO calculate width of rect + if (m_editor.get_properties_panel_visible()) + hint_pos.x += 200.f; if (g_config->editor_autotile_help) { if (m_autotile_mode) @@ -1585,29 +1639,29 @@ EditorOverlayWidget::draw(DrawingContext& context) { if (autotileset) { - context.color().draw_text(Resources::normal_font, fmt::format(fmt::runtime(_("Autotile erasing mode is on (\"{}\")")), autotileset->get_name()) + " " + get_autotileset_key_range(), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_active_color); + context.color().draw_text(Resources::normal_font, fmt::format(fmt::runtime(_("Autotile erasing mode is on (\"{}\")")), autotileset->get_name()) + " " + get_autotileset_key_range(), hint_pos, ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_active_color); } else { - context.color().draw_text(Resources::normal_font, _("Autotile erasing cannot be performed here"), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_error_color); + context.color().draw_text(Resources::normal_font, _("Autotile erasing cannot be performed here"), hint_pos, ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_error_color); } } else if (autotileset) { - context.color().draw_text(Resources::normal_font, fmt::format(fmt::runtime(_("Autotile mode is on (\"{}\")")), autotileset->get_name()) + " " + get_autotileset_key_range(), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_active_color); + context.color().draw_text(Resources::normal_font, fmt::format(fmt::runtime(_("Autotile mode is on (\"{}\")")), autotileset->get_name()) + " " + get_autotileset_key_range(), hint_pos, ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_active_color); } else { - context.color().draw_text(Resources::normal_font, _("Selected tile isn't autotileable"), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_error_color); + context.color().draw_text(Resources::normal_font, _("Selected tile isn't autotileable"), hint_pos, ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_error_color); } } else if (m_editor.get_tiles()->pos(0, 0) == 0) { - context.color().draw_text(Resources::normal_font, _("Hold Ctrl to enable autotile erasing") + " " + get_autotileset_key_range(), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_available_color); + context.color().draw_text(Resources::normal_font, _("Hold Ctrl to enable autotile erasing") + " " + get_autotileset_key_range(), hint_pos, ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_available_color); } else { - context.color().draw_text(Resources::normal_font, _("Hold Ctrl to enable autotile") + " " + get_autotileset_key_range(), Vector(144, 16), ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_available_color); + context.color().draw_text(Resources::normal_font, _("Hold Ctrl to enable autotile") + " " + get_autotileset_key_range(), hint_pos, ALIGN_LEFT, LAYER_OBJECTS+1, EditorOverlayWidget::text_autotile_available_color); } } } @@ -1636,8 +1690,8 @@ Vector EditorOverlayWidget::tile_screen_pos(const Vector& tp, int tile_size) const { Vector sp = tp_to_sp(tp, tile_size); - return (sp - m_editor.get_sector()->get_camera().get_translation()) * - m_editor.get_sector()->get_camera().get_current_scale(); + auto& camera = m_editor.get_sector()->get_camera(); + return (sp - camera.get_translation()) * camera.get_current_scale(); } Vector diff --git a/src/editor/overlay_widget.hpp b/src/editor/overlay_widget.hpp index 635565d262..94b0c0b397 100644 --- a/src/editor/overlay_widget.hpp +++ b/src/editor/overlay_widget.hpp @@ -72,6 +72,11 @@ class EditorOverlayWidget final : public Widget void edit_path(PathGameObject* path, GameObject* new_marked_object = nullptr); //void reset_action_press(); + void draw_tilemap_outer_shading(DrawingContext&); + void draw_tilemap_border(DrawingContext&); + + inline Vector get_sector_pos() const { return m_sector_pos; } + private: static bool action_pressed; static bool alt_pressed; @@ -105,7 +110,6 @@ class EditorOverlayWidget final : public Widget void draw_tile_tip(DrawingContext&); void draw_tile_grid(DrawingContext&, int tile_size, bool draw_shadow) const; - void draw_tilemap_border(DrawingContext&); void draw_path(DrawingContext&); void draw_rectangle_preview(DrawingContext& context); @@ -139,6 +143,7 @@ class EditorOverlayWidget final : public Widget Vector m_sector_pos; Vector m_mouse_pos; Vector m_previous_mouse_pos; + Vector m_last_target_pos; std::chrono::steady_clock::time_point m_time_prev_put_tile; diff --git a/src/editor/particle_editor.cpp b/src/editor/particle_editor.cpp index 13460c4338..3362c3c592 100644 --- a/src/editor/particle_editor.cpp +++ b/src/editor/particle_editor.cpp @@ -112,7 +112,7 @@ ParticleEditor::reset_main_ui() // TODO: Use the addButton() command // Texture button start auto texture_btn = std::make_unique(_("Change texture... ->")); - texture_btn.get()->m_on_change = std::function([this](){ + texture_btn.get()->m_on_activate_callbacks.emplace_back([this](){ m_in_texture_tab = true; }); float tmp_height = 0.f; @@ -227,7 +227,7 @@ ParticleEditor::reset_main_ui() // TODO: add some ParticleEditor::addButton() function so that I don't have to put all that in here auto clear_btn = std::make_unique(_("Clear")); - clear_btn.get()->m_on_change = std::function([this](){ m_particles->clear(); }); + clear_btn.get()->m_on_activate_callbacks.emplace_back([this](){ m_particles->clear(); }); float height = 0.f; for (const auto& control : m_controls) { height = std::max(height, control->get_rect().get_bottom() + 5.f); @@ -243,7 +243,7 @@ ParticleEditor::reset_texture_ui() m_texture_rebinds.clear(); auto return_btn = std::make_unique(_("<- General settings")); - return_btn.get()->m_on_change = std::function([this](){ + return_btn.get()->m_on_activate_callbacks.emplace_back([this](){ m_in_texture_tab = false; }); return_btn.get()->set_rect(Rectf(25.f, 0.f, 325.f, 20.f)); @@ -253,7 +253,7 @@ ParticleEditor::reset_texture_ui() likeliness_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->likeliness)); likeliness_control.get()->set_rect(Rectf(150.f, 50.f, 350.f, 70.f)); likeliness_control.get()->m_label = std::make_unique(Rectf(5.f, 50.f, 135.f, 70.f), _("Likeliness")); - likeliness_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + likeliness_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); auto likeliness_control_ptr = likeliness_control.get(); m_texture_rebinds.push_back( [this, likeliness_control_ptr]{ likeliness_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->likeliness)); @@ -264,7 +264,7 @@ ParticleEditor::reset_texture_ui() color_r_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.red)); color_r_control.get()->set_rect(Rectf(150.f, 80.f, 192.f, 100.f)); color_r_control.get()->m_label = std::make_unique(Rectf(5.f, 80.f, 140.f, 100.f), _("Color (RGBA)")); - color_r_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + color_r_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); color_r_control.get()->m_validate_float = m_clamp_0_1; auto color_r_control_ptr = color_r_control.get(); m_texture_rebinds.push_back( [this, color_r_control_ptr]{ @@ -275,7 +275,7 @@ ParticleEditor::reset_texture_ui() auto color_g_control = std::make_unique(); color_g_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.green)); color_g_control.get()->set_rect(Rectf(202.f, 80.f, 245.f, 100.f)); - color_g_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + color_g_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); color_g_control.get()->m_validate_float = m_clamp_0_1; auto color_g_control_ptr = color_g_control.get(); m_texture_rebinds.push_back( [this, color_g_control_ptr]{ @@ -286,7 +286,7 @@ ParticleEditor::reset_texture_ui() auto color_b_control = std::make_unique(); color_b_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.blue)); color_b_control.get()->set_rect(Rectf(255.f, 80.f, 297.f, 100.f)); - color_b_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + color_b_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); color_b_control.get()->m_validate_float = m_clamp_0_1; auto color_b_control_ptr = color_b_control.get(); m_texture_rebinds.push_back( [this, color_b_control_ptr]{ @@ -297,7 +297,7 @@ ParticleEditor::reset_texture_ui() auto color_a_control = std::make_unique(); color_a_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.alpha)); color_a_control.get()->set_rect(Rectf(307.f, 80.f, 350.f, 100.f)); - color_a_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + color_a_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); color_a_control.get()->m_validate_float = m_clamp_0_1; auto color_a_control_ptr = color_a_control.get(); m_texture_rebinds.push_back( [this, color_a_control_ptr]{ @@ -309,7 +309,7 @@ ParticleEditor::reset_texture_ui() scale_x_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->scale.x)); scale_x_control.get()->set_rect(Rectf(150.f, 110.f, 240.f, 130.f)); scale_x_control.get()->m_label = std::make_unique(Rectf(5.f, 110.f, 150.f, 130.f), _("Scale (x, y)")); - scale_x_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + scale_x_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); auto scale_x_control_ptr = scale_x_control.get(); m_texture_rebinds.push_back( [this, scale_x_control_ptr]{ scale_x_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->scale.x)); @@ -319,7 +319,7 @@ ParticleEditor::reset_texture_ui() auto scale_y_control = std::make_unique(); scale_y_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->scale.y)); scale_y_control.get()->set_rect(Rectf(260.f, 110.f, 350.f, 130.f)); - scale_y_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + scale_y_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); auto scale_y_control_ptr = scale_y_control.get(); m_texture_rebinds.push_back( [this, scale_y_control_ptr]{ scale_y_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->scale.y)); @@ -330,7 +330,7 @@ ParticleEditor::reset_texture_ui() hb_scale_x_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->hb_scale.x)); hb_scale_x_control.get()->set_rect(Rectf(150.f, 140.f, 240.f, 160.f)); hb_scale_x_control.get()->m_label = std::make_unique(Rectf(5.f, 140.f, 150.f, 160.f), _("Hitbox scale (x, y)")); - hb_scale_x_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + hb_scale_x_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); auto hb_scale_x_control_ptr = hb_scale_x_control.get(); m_texture_rebinds.push_back( [this, hb_scale_x_control_ptr]{ hb_scale_x_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->hb_scale.x)); @@ -340,7 +340,7 @@ ParticleEditor::reset_texture_ui() auto hb_scale_y_control = std::make_unique(); hb_scale_y_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->hb_scale.y)); hb_scale_y_control.get()->set_rect(Rectf(260.f, 140.f, 350.f, 160.f)); - hb_scale_y_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + hb_scale_y_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); auto hb_scale_y_control_ptr = hb_scale_y_control.get(); m_texture_rebinds.push_back( [this, hb_scale_y_control_ptr]{ hb_scale_y_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->hb_scale.y)); @@ -351,7 +351,7 @@ ParticleEditor::reset_texture_ui() hb_offset_x_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->hb_offset.x)); hb_offset_x_control.get()->set_rect(Rectf(150.f, 170.f, 240.f, 190.f)); hb_offset_x_control.get()->m_label = std::make_unique(Rectf(5.f, 170.f, 150.f, 190.f), _("Hitbox offset relative to scale")); - hb_offset_x_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + hb_offset_x_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); auto hb_offset_x_control_ptr = hb_offset_x_control.get(); m_texture_rebinds.push_back( [this, hb_offset_x_control_ptr]{ hb_offset_x_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->hb_offset.x)); @@ -361,7 +361,7 @@ ParticleEditor::reset_texture_ui() auto hb_offset_y_control = std::make_unique(); hb_offset_y_control.get()->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->hb_offset.y)); hb_offset_y_control.get()->set_rect(Rectf(260.f, 170.f, 350.f, 190.f)); - hb_offset_y_control.get()->m_on_change = std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + hb_offset_y_control.get()->m_on_change_callbacks.emplace_back([this](){ m_particles->reinit_textures(); this->push_version(); }); auto hb_offset_y_control_ptr = hb_offset_y_control.get(); m_texture_rebinds.push_back( [this, hb_offset_y_control_ptr]{ hb_offset_y_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->hb_offset.y)); @@ -370,7 +370,7 @@ ParticleEditor::reset_texture_ui() // Texture button start auto chg_texture_btn = std::make_unique(_("Change texture...")); - chg_texture_btn.get()->m_on_change = std::function([this](){ + chg_texture_btn.get()->m_on_activate_callbacks.emplace_back([this](){ const std::vector& filter = {".jpg", ".png", ".surface"}; MenuManager::instance().push_menu(std::make_unique( nullptr, @@ -389,7 +389,7 @@ ParticleEditor::reset_texture_ui() // Texture button end auto prev_btn = std::make_unique("<"); - prev_btn.get()->m_on_change = std::function([this](){ + prev_btn.get()->m_on_activate_callbacks.emplace_back([this](){ m_texture_current--; if (m_texture_current < 0) m_texture_current = 0; for (const auto& refresh : m_texture_rebinds) @@ -399,7 +399,7 @@ ParticleEditor::reset_texture_ui() m_controls_textures.push_back(std::move(prev_btn)); auto del_btn = std::make_unique("-"); - del_btn.get()->m_on_change = std::function([this](){ + del_btn.get()->m_on_activate_callbacks.emplace_back([this](){ if (m_particles->m_textures.size() < 1) return; m_particles->m_textures.erase(m_particles->m_textures.begin() + m_texture_current); @@ -414,7 +414,7 @@ ParticleEditor::reset_texture_ui() m_controls_textures.push_back(std::move(del_btn)); auto add_btn = std::make_unique("+"); - add_btn.get()->m_on_change = std::function([this](){ + add_btn.get()->m_on_activate_callbacks.emplace_back([this](){ m_particles->m_textures.push_back(CustomParticleSystem::SpriteProperties()); m_texture_current = static_cast(m_particles->m_textures.size()) - 1; for (const auto& refresh : m_texture_rebinds) @@ -426,7 +426,7 @@ ParticleEditor::reset_texture_ui() m_controls_textures.push_back(std::move(add_btn)); auto next_btn = std::make_unique(">"); - next_btn.get()->m_on_change = std::function([this](){ + next_btn.get()->m_on_activate_callbacks.emplace_back([this](){ m_texture_current++; if (m_texture_current > static_cast(m_particles->m_textures.size()) - 1) m_texture_current = static_cast(m_particles->m_textures.size()) - 1; @@ -473,8 +473,8 @@ ParticleEditor::addTextboxFloatWithImprecision(const std::string& name, float* b float_control.get()->m_label = std::make_unique(Rectf(5.f, height, 145.f, height + 20.f), name); imp_control.get()->m_label = std::make_unique(Rectf(240.f, height, 260.f, height + 20.f), "±"); - float_control.get()->m_on_change = std::function([this](){ this->push_version(); }); - imp_control.get()->m_on_change = std::function([this](){ this->push_version(); }); + float_control.get()->m_on_change_callbacks.emplace_back([this](){ this->push_version(); }); + imp_control.get()->m_on_change_callbacks.emplace_back([this](){ this->push_version(); }); m_controls.push_back(std::move(float_control)); m_controls.push_back(std::move(imp_control)); @@ -556,7 +556,7 @@ ParticleEditor::addControl(std::string name, std::unique_ptr n } new_control.get()->m_label = std::make_unique(Rectf(5.f, height, 135.f, height + 20.f), std::move(name)); - new_control.get()->m_on_change = std::function([this](){ this->push_version(); }); + new_control.get()->m_on_change_callbacks.emplace_back([this](){ this->push_version(); }); m_controls.push_back(std::move(new_control)); } @@ -734,9 +734,7 @@ ParticleEditor::quit_editor() auto quit = [] () { ScreenManager::current()->pop_screen(); - if (Editor::current()) { - Editor::current()->m_reactivate_request = true; - } + Editor::may_reactivate(); }; check_unsaved_changes([quit] { diff --git a/src/editor/tilebox.cpp b/src/editor/tilebox.cpp index f6d071e26a..8ec0bab1a3 100644 --- a/src/editor/tilebox.cpp +++ b/src/editor/tilebox.cpp @@ -21,6 +21,8 @@ #include "editor/object_info.hpp" #include "editor/tile_selection.hpp" #include "editor/tip.hpp" +#include "sprite/sprite_ptr.hpp" +#include "sprite/sprite_manager.hpp" #include "supertux/colorscheme.hpp" #include "supertux/debug.hpp" #include "supertux/gameconfig.hpp" @@ -30,35 +32,41 @@ #include "supertux/resources.hpp" #include "util/log.hpp" #include "video/drawing_context.hpp" +#include "video/video_system.hpp" +#include EditorTilebox::EditorTilebox(Editor& editor, const Rectf& rect) : m_editor(editor), m_rect(rect), m_tiles(new TileSelection()), m_object(), + m_tilegroup_id(0), + m_objectgroup_id(0), m_object_tip(new Tip()), m_input_type(InputType::NONE), m_active_tilegroup(), m_active_objectgroup(), m_object_info(new ObjectInfo()), m_on_select_callback([](EditorTilebox&) {}), - m_scrollbar(), + m_scrollbar(new ControlScrollbar(1.f, 1.f, m_scroll_progress, 35.f)), m_scroll_progress(1.f), m_hovered_item(HoveredItem::NONE), m_hovered_tile(-1), m_dragging(false), m_drag_start(0, 0), - m_mouse_pos(0, 0) + m_mouse_pos(0, 0), + m_shadow(SpriteManager::current()->create("images/engine/editor/shadow2.png")) { - m_scrollbar.reset(new ControlScrollbar(1.f, 1.f, m_scroll_progress, 35.f)); } void EditorTilebox::draw(DrawingContext& context) { + context.color().set_blur(g_config->editor_blur); context.color().draw_filled_rect(m_rect, g_config->editorcolor, 0.0f, LAYER_GUI-10); + context.color().set_blur(0); if (m_dragging) { @@ -75,6 +83,18 @@ EditorTilebox::draw(DrawingContext& context) 0.0f, LAYER_GUI - 5); } + // Shadow + constexpr float SCROLL_SHADOW_MAX = 10.f; + float scroll_shadow_size = std::clamp(m_scroll_progress * 0.1, 0.f, SCROLL_SHADOW_MAX); + float scroll_shadow_normal = scroll_shadow_size / SCROLL_SHADOW_MAX; + + context.set_alpha(scroll_shadow_normal * 0.3); + m_shadow->draw_scaled( + context.color(), + Rectf{m_rect.get_left(), m_rect.get_top(), m_rect.get_right(), m_rect.get_top() + scroll_shadow_size}, + LAYER_GUI+1); + context.set_alpha(1.0); + context.push_transform(); context.set_viewport(Rect(m_rect)); switch (m_input_type) @@ -160,6 +180,7 @@ EditorTilebox::selection_draw_rect() const if (select.get_top() < m_rect.get_top()) // Do not go outside toolbox select.set_top(m_rect.get_top()); + Editor::current()->m_tilebox_something_selected = true; return select; } @@ -373,18 +394,70 @@ void EditorTilebox::select_tilegroup(int id) { m_active_tilegroup.reset(new Tilegroup(m_editor.get_tileset()->get_tilegroups()[id])); + m_tilegroup_id = id; m_input_type = InputType::TILE; reset_scrollbar(); } +void +EditorTilebox::select_last_tilegroup() +{ + select_tilegroup(get_tilegroup_id()); +} + void EditorTilebox::select_objectgroup(int id) { m_active_objectgroup = &m_object_info->m_groups[id]; + m_objectgroup_id = id; m_input_type = InputType::OBJECT; reset_scrollbar(); } +void +EditorTilebox::select_last_objectgroup() +{ + select_objectgroup(m_objectgroup_id); +} + +void +EditorTilebox::change_tilegroup(int dir) +{ + if (m_input_type == InputType::OBJECT) + { + select_last_tilegroup(); + return; + } + + m_tilegroup_id += dir; + size_t tilegroups_size = m_editor.get_tileset()->get_tilegroups().size(); + if (m_tilegroup_id < 0) + m_tilegroup_id = tilegroups_size - 1; + else if (m_tilegroup_id > tilegroups_size - 1) + m_tilegroup_id = 0; + + select_last_tilegroup(); +} + +void +EditorTilebox::change_objectgroup(int dir) +{ + if (m_input_type == InputType::TILE) + { + select_last_objectgroup(); + return; + } + + m_objectgroup_id += dir; + size_t objectgroups_size = m_object_info->m_groups.size(); + if (m_objectgroup_id < 0) + m_objectgroup_id = objectgroups_size - 1; + else if (m_objectgroup_id > objectgroups_size - 1) + m_objectgroup_id = 0; + + select_last_objectgroup(); +} + bool EditorTilebox::select_layers_objectgroup() { diff --git a/src/editor/tilebox.hpp b/src/editor/tilebox.hpp index c76e4a324e..02c0310764 100644 --- a/src/editor/tilebox.hpp +++ b/src/editor/tilebox.hpp @@ -25,6 +25,8 @@ #include "interface/control_scrollbar.hpp" #include "math/rectf.hpp" #include "math/vector.hpp" +#include "sprite/sprite.hpp" +#include "sprite/sprite_ptr.hpp" #include "supertux/tile_set.hpp" class Editor; @@ -74,8 +76,10 @@ class EditorTilebox final : public Widget void on_select(const std::function& callback); void select_tilegroup(int id); + void select_last_tilegroup(); inline void set_tilegroup(std::unique_ptr tilegroup) { m_active_tilegroup = std::move(tilegroup); } void select_objectgroup(int id); + void select_last_objectgroup(); bool select_layers_objectgroup(); inline const ObjectInfo& get_object_info() const { return *m_object_info; } @@ -89,6 +93,11 @@ class EditorTilebox final : public Widget float get_tiles_height() const; inline bool has_active_object_tip() const { return m_object_tip->get_visible(); } + inline size_t get_objectgroup_id() const { return m_objectgroup_id; } + inline size_t get_tilegroup_id() const { return m_tilegroup_id; } + + void change_tilegroup(int dir); + void change_objectgroup(int dir); private: Vector get_tile_coords(int pos, bool relative = true) const; @@ -120,6 +129,7 @@ class EditorTilebox final : public Widget std::unique_ptr m_active_tilegroup; ObjectGroup* m_active_objectgroup; std::unique_ptr m_object_info; + int m_tilegroup_id, m_objectgroup_id; std::function m_on_select_callback; @@ -133,6 +143,8 @@ class EditorTilebox final : public Widget Vector m_drag_start; Vector m_mouse_pos; + + SpritePtr m_shadow; private: EditorTilebox(const EditorTilebox&) = delete; diff --git a/src/editor/tool_icon.cpp b/src/editor/tool_icon.cpp index 01ea1e79e3..a8482f1ab5 100644 --- a/src/editor/tool_icon.cpp +++ b/src/editor/tool_icon.cpp @@ -45,8 +45,16 @@ ToolIcon::draw(DrawingContext& context) void ToolIcon::next_mode() { - m_mode++; - if (m_mode >= m_surf_count) { + set_mode(m_mode + 1); +} + +void +ToolIcon::set_mode(int mode) +{ + m_mode = mode; + + if (m_mode >= m_surf_count) + { m_mode = 0; } } diff --git a/src/editor/tool_icon.hpp b/src/editor/tool_icon.hpp index 911ed8c5af..d94efc8963 100644 --- a/src/editor/tool_icon.hpp +++ b/src/editor/tool_icon.hpp @@ -32,6 +32,7 @@ class ToolIcon final void draw(DrawingContext& context); inline int get_mode() const { return m_mode; } + void set_mode(int mode); void next_mode(); diff --git a/src/editor/toolbox_widget.cpp b/src/editor/toolbox_widget.cpp index 8565f6c845..0e4315cdef 100644 --- a/src/editor/toolbox_widget.cpp +++ b/src/editor/toolbox_widget.cpp @@ -22,6 +22,7 @@ #include "editor/tool_icon.hpp" #include "gui/menu_manager.hpp" #include "gui/mousecursor.hpp" +#include "gui/menu.hpp" #include "supertux/colorscheme.hpp" #include "supertux/gameconfig.hpp" #include "supertux/globals.hpp" @@ -54,17 +55,21 @@ EditorToolboxWidget::EditorToolboxWidget(Editor& editor) : m_move_mode->push_mode("images/engine/editor/move-mode1.png"); m_undo_mode->push_mode("images/engine/editor/redo.png"); //settings_mode->push_mode("images/engine/editor/settings-mode1.png"); + + set_mouse_tool(); } void EditorToolboxWidget::draw(DrawingContext& context) { m_tilebox->draw(context); - + + context.color().set_blur(g_config->editor_blur); context.color().draw_filled_rect(Rectf(Vector(m_pos_x, 0.f), Vector(context.get_width(), 96.f)), g_config->editorcolor, 0.0f, LAYER_GUI-10); + context.color().set_blur(0); if (m_hovered_item != HoveredItem::NONE && m_hovered_item != HoveredItem::TILEBOX) { @@ -124,6 +129,7 @@ EditorToolboxWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) { m_editor.disable_keyboard(); MenuManager::instance().push_menu(MenuStorage::EDITOR_TILEGROUP_MENU); + MenuManager::instance().current_menu()->set_item(m_tilebox->get_tilegroup_id()); } else { @@ -137,6 +143,7 @@ EditorToolboxWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) { m_editor.disable_keyboard(); MenuManager::instance().push_menu(MenuStorage::EDITOR_OBJECTGROUP_MENU); + MenuManager::instance().current_menu()->set_item(m_tilebox->get_objectgroup_id()); } else { @@ -151,10 +158,7 @@ EditorToolboxWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) switch (m_hovered_tool) { case 0: - m_tilebox->get_tiles()->set_tile(0); - m_tilebox->set_object(""); - m_editor.update_autotileset(); - update_mouse_icon(); + set_rubber_tool(); break; case 1: @@ -181,8 +185,7 @@ EditorToolboxWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) break; case 3: - m_tilebox->set_object("#move"); - update_mouse_icon(); + set_mouse_tool(); break; default: @@ -200,6 +203,22 @@ EditorToolboxWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) } } +void +EditorToolboxWidget::set_rubber_tool() +{ + m_tilebox->set_object(""); + m_tilebox->get_tiles()->set_tile(0); + m_editor.update_autotileset(); + update_mouse_icon(); +} + +void +EditorToolboxWidget::set_mouse_tool() +{ + m_tilebox->set_object("#move"); + update_mouse_icon(); +} + bool EditorToolboxWidget::on_mouse_motion(const SDL_MouseMotionEvent& motion) { @@ -252,6 +271,37 @@ EditorToolboxWidget::on_mouse_motion(const SDL_MouseMotionEvent& motion) bool EditorToolboxWidget::on_mouse_wheel(const SDL_MouseWheelEvent& wheel) { + switch (m_hovered_item) + { + case HoveredItem::TILEGROUP: + if (m_editor.get_tileset()->get_tilegroups().size() > 1) + { + m_tilebox->change_tilegroup(wheel.y > 0 ? -1 : 1); + } + else + { + select_tilegroup(0); + } + break; + + case HoveredItem::OBJECTS: + if ((m_editor.get_level()->is_worldmap() && m_tilebox->get_object_info().get_num_worldmap_groups() > 1) || + (!m_editor.get_level()->is_worldmap() && m_tilebox->get_object_info().get_num_level_groups() > 1)) + { + m_tilebox->change_objectgroup(wheel.y > 0 ? -1 : 1); + } + else + { + if (m_editor.get_level()->is_worldmap()) + select_objectgroup(m_tilebox->get_object_info().get_first_worldmap_group_index()); + else + select_objectgroup(0); + } + break; + + default: + break; + } return m_tilebox->on_mouse_wheel(wheel); } @@ -294,18 +344,44 @@ EditorToolboxWidget::select_objectgroup(int id) update_mouse_icon(); } +void +EditorToolboxWidget::select_last_tilegroup() +{ + m_tilebox->select_last_tilegroup(); +} + +void +EditorToolboxWidget::select_last_objectgroup() +{ + m_tilebox->select_last_objectgroup(); +} + int EditorToolboxWidget::get_tileselect_select_mode() const { return m_select_mode->get_mode(); } +void +EditorToolboxWidget::set_tileselect_select_mode(int mode) +{ + m_select_mode->set_mode(mode); + update_mouse_icon(); +} + int EditorToolboxWidget::get_tileselect_move_mode() const { return m_move_mode->get_mode(); } +void +EditorToolboxWidget::set_tileselect_move_mode(int mode) +{ + m_move_mode->set_mode(mode); + update_mouse_icon(); +} + Vector EditorToolboxWidget::get_tool_coords(int pos) const { diff --git a/src/editor/toolbox_widget.hpp b/src/editor/toolbox_widget.hpp index f337191fcf..648df4e629 100644 --- a/src/editor/toolbox_widget.hpp +++ b/src/editor/toolbox_widget.hpp @@ -54,11 +54,19 @@ class EditorToolboxWidget final : public Widget void select_tilegroup(int id); void select_objectgroup(int id); + void select_last_tilegroup(); + void select_last_objectgroup(); int get_tileselect_select_mode() const; int get_tileselect_move_mode() const; void update_mouse_icon(); + void set_tileselect_move_mode(int mode); + + void set_tileselect_select_mode(int mode); + + void set_mouse_tool(); + void set_rubber_tool(); inline EditorTilebox& get_tilebox() const { return *m_tilebox; } diff --git a/src/gui/item_script.cpp b/src/gui/item_script.cpp index 9a3a8db02f..94e4cceaa5 100644 --- a/src/gui/item_script.cpp +++ b/src/gui/item_script.cpp @@ -20,15 +20,17 @@ #include "gui/menu_manager.hpp" #include "gui/menu_script.hpp" -ItemScript::ItemScript(const std::string& text, std::string* script_, int id) : +ItemScript::ItemScript(UID uid, const std::string& key, const std::string& text, std::string* script_, int id) : MenuItem(text, id), - script(script_) + script(script_), + m_key(std::move(key)), + m_uid(std::move(uid)) { } void ItemScript::process_action(const MenuAction& action) { if (action == MenuAction::HIT) { - MenuManager::instance().push_menu(std::make_unique(script)); + MenuManager::instance().push_menu(std::make_unique(m_uid, m_key, script)); } } diff --git a/src/gui/item_script.hpp b/src/gui/item_script.hpp index fd3d792a09..3add430096 100644 --- a/src/gui/item_script.hpp +++ b/src/gui/item_script.hpp @@ -21,13 +21,15 @@ class ItemScript final : public MenuItem { public: - ItemScript(const std::string& text_, std::string* script_, int id = -1); + ItemScript(UID uid, const std::string& key, const std::string& text_, std::string* script_, int id = -1); /** Processes the menu action. */ virtual void process_action(const MenuAction& action) override; private: std::string* script; + std::string m_key; + UID m_uid; private: ItemScript(const ItemScript&) = delete; diff --git a/src/gui/menu.cpp b/src/gui/menu.cpp index cf14843af5..35a605848e 100644 --- a/src/gui/menu.cpp +++ b/src/gui/menu.cpp @@ -55,6 +55,9 @@ #include "supertux/error_handler.hpp" +// The amount in pixels the mouse has to wiggle after scrolling before it can hover over things again. +constexpr int MOUSE_DEADZONE_AMOUNT = 70; + Menu::Menu() : m_pos(Vector(static_cast(SCREEN_WIDTH) / 2.0f, static_cast(SCREEN_HEIGHT) / 2.0f)), @@ -65,7 +68,9 @@ Menu::Menu() : m_menu_help_height(0.0f), m_items(), m_arrange_left(0), - m_active_item(-1) + m_active_item(-1), + m_mouse_deadzone(0), + m_can_click_when_unfocused(false) { } @@ -162,9 +167,9 @@ Menu::add_textfield(const std::string& text, std::string* input, int id) } ItemScript& -Menu::add_script(const std::string& text, std::string* script, int id) +Menu::add_script(UID uid, const std::string& key, const std::string& text, std::string* script, int id) { - return add_item(text, script, id); + return add_item(uid, key, text, script, id); } ItemIntField& @@ -327,6 +332,24 @@ Menu::clear() m_active_item = -1; } +void +Menu::previous_item() +{ + if (m_active_item > 0) + --m_active_item; + else + m_active_item = m_items.size() - 1; +} + +void +Menu::next_item() +{ + if (m_active_item < m_items.size() - 1) + ++m_active_item; + else + m_active_item = 0; +} + void Menu::process_action(const MenuAction& action) { @@ -370,20 +393,14 @@ Menu::process_action(const MenuAction& action) switch (action) { case MenuAction::UP: do { - if (m_active_item > 0) - --m_active_item; - else - m_active_item = int(m_items.size())-1; + previous_item(); } while (m_items[m_active_item]->skippable() && (m_active_item != last_active_item)); break; case MenuAction::DOWN: do { - if (m_active_item < int(m_items.size())-1 ) - ++m_active_item; - else - m_active_item = 0; + next_item(); } while (m_items[m_active_item]->skippable() && (m_active_item != last_active_item)); break; @@ -542,6 +559,34 @@ Menu::draw(DrawingContext& context) } } +void +Menu::set_item(int index) +{ + if (index < 0) + index = 0; + + m_active_item = 0; + + // Attempt to skip all skippable items + do + { + if (m_active_item > m_items.size()) + break; + + if (m_items[m_active_item]->skippable()) + { + ++m_active_item; + continue; + } + + if (index > 0) + ++m_active_item; + + --index; + } + while (index >= 0); +} + MenuItem& Menu::get_item_by_id(int id) { @@ -590,16 +635,31 @@ Menu::event(const SDL_Event& ev) calculate_width(); } break; + + case SDL_MOUSEWHEEL: + { + if (ev.wheel.y > 0) + { + do { previous_item(); } while (m_items[m_active_item]->skippable()); + } + else { + do { next_item(); } while (m_items[m_active_item]->skippable()); + } + m_mouse_deadzone = MOUSE_DEADZONE_AMOUNT; + } + break; case SDL_MOUSEBUTTONDOWN: if (ev.button.button == SDL_BUTTON_LEFT) { Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(ev.motion.x, ev.motion.y); - if (mouse_pos.x > m_pos.x - get_width() / 2.0f && - mouse_pos.x < m_pos.x + get_width() / 2.0f && - mouse_pos.y > m_pos.y - get_height() / 2.0f && - mouse_pos.y < m_pos.y + get_height() / 2.0f) + if ((mouse_pos.x > m_pos.x - get_width() / 2.0f && + mouse_pos.x < m_pos.x + get_width() / 2.0f && + mouse_pos.y > m_pos.y - get_height() / 2.0f && + mouse_pos.y < m_pos.y + get_height() / 2.0f) || + m_mouse_deadzone > 0 || + m_can_click_when_unfocused) { process_action(MenuAction::HIT); } @@ -608,6 +668,15 @@ Menu::event(const SDL_Event& ev) case SDL_MOUSEMOTION: { + if (m_mouse_deadzone > 0) + { + m_mouse_deadzone -= abs(ev.motion.xrel); + m_mouse_deadzone -= abs(ev.motion.yrel); + + if (m_mouse_deadzone < 0) + m_mouse_deadzone = 0; + return; + } Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(ev.motion.x, ev.motion.y); float x = mouse_pos.x; float y = mouse_pos.y; diff --git a/src/gui/menu.hpp b/src/gui/menu.hpp index 4d7ff1e4a7..23e2cf677f 100644 --- a/src/gui/menu.hpp +++ b/src/gui/menu.hpp @@ -22,6 +22,7 @@ #include "gui/menu_action.hpp" #include "math/vector.hpp" +#include "util/uid.hpp" #include "video/color.hpp" #include "video/drawing_context.hpp" @@ -90,7 +91,7 @@ class Menu ItemStringSelect& add_string_select(int id, const std::string& text, int* selected, const std::vector& strings); ItemStringSelect& add_string_select(int id, const std::string& text, int default_item, const std::vector& strings); ItemTextField& add_textfield(const std::string& text, std::string* input, int id = -1); - ItemScript& add_script(const std::string& text, std::string* script, int id = -1); + ItemScript& add_script(UID uid, const std::string& key, const std::string& text, std::string* script, int id = -1); ItemIntField& add_intfield(const std::string& text, int* input, int id = -1, bool positive = false); ItemFloatField& add_floatfield(const std::string& text, float* input, int id = -1, bool positive = false); ItemAction& add_file(const std::string& text, std::string* input, const std::vector& extensions, @@ -112,6 +113,7 @@ class Menu /** Remove all entries from the menu */ void clear(); + void set_item(int index); MenuItem& get_item(int index) { return *(m_items[index]); } MenuItem& get_item_by_id(int id); @@ -122,13 +124,18 @@ class Menu inline Vector get_center_pos() const { return m_pos; } inline void set_center_pos(float x, float y) { m_pos.x = x; m_pos.y = y; } + + void previous_item(); + void next_item(); float get_width() const; float get_height() const; /** returns true when the text is more important than action */ virtual bool is_sensitive() const { return false; } - + + inline void allow_click_when_unfocused() { m_can_click_when_unfocused = true; } + protected: MenuItem& add_item(std::unique_ptr menu_item); MenuItem& add_item(std::unique_ptr menu_item, int pos_); @@ -161,6 +168,8 @@ class Menu float m_menu_width; float m_menu_height; float m_menu_help_height; + int m_mouse_deadzone; + bool m_can_click_when_unfocused; public: std::vector > m_items; diff --git a/src/gui/menu_manager.cpp b/src/gui/menu_manager.cpp index f8fdd35fd2..22a26ed096 100644 --- a/src/gui/menu_manager.cpp +++ b/src/gui/menu_manager.cpp @@ -18,6 +18,7 @@ #include "gui/menu_manager.hpp" #include "control/input_manager.hpp" +#include "editor/editor.hpp" #include "gui/dialog.hpp" #include "gui/menu.hpp" #include "gui/mousecursor.hpp" @@ -190,7 +191,9 @@ MenuManager::draw(DrawingContext& context) } if (m_notification.current) // Has current notification + { m_notification.current->draw(context); + } if ((has_dialog() || is_active()) && MouseCursor::current()) // Cursor should be drawn MouseCursor::current()->draw(context); @@ -227,12 +230,16 @@ MenuManager::set_menu(std::unique_ptr menu, bool skip_transition) transition(m_menu_stack.empty() ? nullptr : m_menu_stack.back().get(), menu.get()); m_menu_stack.clear(); m_menu_stack.push_back(std::move(menu)); + + Editor::may_deactivate(); } else { if (!skip_transition) transition(m_menu_stack.empty() ? nullptr : m_menu_stack.back().get(), nullptr); m_menu_stack.clear(); + + Editor::may_reactivate(); } // just to be sure... @@ -252,6 +259,8 @@ MenuManager::push_menu(std::unique_ptr menu, bool skip_transition) if (!skip_transition) transition(m_menu_stack.empty() ? nullptr : m_menu_stack.back().get(), menu.get()); m_menu_stack.push_back(std::move(menu)); + + Editor::may_deactivate(); } void @@ -267,6 +276,9 @@ MenuManager::pop_menu(bool skip_transition) transition(m_menu_stack.back().get(), m_menu_stack.size() >= 2 ? m_menu_stack[m_menu_stack.size() - 2].get() : nullptr); m_menu_stack.pop_back(); + + if (m_menu_stack.empty()) + Editor::may_reactivate(); } void @@ -275,6 +287,8 @@ MenuManager::clear_menu_stack(bool skip_transition) if (!skip_transition) transition(m_menu_stack.empty() ? nullptr : m_menu_stack.back().get(), nullptr); m_menu_stack.clear(); + + Editor::may_reactivate(); } diff --git a/src/gui/menu_script.cpp b/src/gui/menu_script.cpp index d044a035c1..bdaabb9f33 100644 --- a/src/gui/menu_script.cpp +++ b/src/gui/menu_script.cpp @@ -16,12 +16,16 @@ #include "gui/menu_script.hpp" +#include "editor/editor.hpp" #include "gui/item_script_line.hpp" #include "util/gettext.hpp" -ScriptMenu::ScriptMenu(std::string* script_) : +ScriptMenu::ScriptMenu(UID uid, const std::string& key, std::string* script_) : base_script(script_), - script_strings() + script_strings(), + m_start_time(time(0)), + m_key(key), + m_uid(uid) { script_strings.clear(); @@ -42,12 +46,24 @@ ScriptMenu::ScriptMenu(std::string* script_) : //add_script_line(base_script); + if (Editor::current()) + Editor::current()->m_script_manager.register_script(m_uid, base_script); + add_hl(); + add_entry(_("Open in editor"), [this]{ + FileSystem::open_editor(ScriptManager::full_filename_from_key(m_uid)); + }); add_back(_("OK")); } ScriptMenu::~ScriptMenu() { + time_t mtime = Editor::current()->m_script_manager.get_mtime(m_uid); + + // Don't save if the external file was edited. + if (mtime > m_start_time) + return; + *base_script = *(script_strings[0]); for (auto i = script_strings.begin()+1; i != script_strings.end(); ++i) { *base_script += "\n" + **i; diff --git a/src/gui/menu_script.hpp b/src/gui/menu_script.hpp index 9c55a0f35c..fc9274716e 100644 --- a/src/gui/menu_script.hpp +++ b/src/gui/menu_script.hpp @@ -16,6 +16,7 @@ #pragma once +#include #include "gui/menu.hpp" class ItemScriptLine; @@ -23,7 +24,7 @@ class ItemScriptLine; class ScriptMenu final : public Menu { public: - ScriptMenu(std::string* script_); + ScriptMenu(UID uid, const std::string& key, std::string* script_); ~ScriptMenu() override; void menu_action(MenuItem& item) override; @@ -35,8 +36,11 @@ class ScriptMenu final : public Menu bool is_sensitive() const override; private: + time_t m_start_time; std::string* base_script; std::vector > script_strings; + std::string m_key; + UID m_uid; void push_string(const std::string& new_line); diff --git a/src/gui/notification.cpp b/src/gui/notification.cpp index e137746321..c8d367eead 100644 --- a/src/gui/notification.cpp +++ b/src/gui/notification.cpp @@ -17,6 +17,7 @@ #include "gui/notification.hpp" #include "control/controller.hpp" +#include "editor/editor.hpp" #include "gui/menu_manager.hpp" #include "gui/mousecursor.hpp" #include "supertux/colorscheme.hpp" @@ -29,17 +30,29 @@ #include "video/viewport.hpp" #include "util/gettext.hpp" #include "util/log.hpp" +#include -Notification::Notification(std::string id, bool no_auto_hide, bool no_auto_disable) : +constexpr float DRAG_DEADZONE = 10.f; +constexpr float DRAG_MAX = 120.f; + +Notification::Notification(const std::string& id, float idle_close_time, + bool no_auto_close, bool auto_disable) : m_id(id), - m_auto_hide(!no_auto_hide), - m_auto_disable(!no_auto_disable), + m_idle_close_time(idle_close_time), + m_auto_close(!no_auto_close), + m_auto_disable(auto_disable), + m_idle_close_timer(), + m_alpha(1.f), m_text(), m_mini_text(), m_text_size(), m_mini_text_size(), + m_init_mouse_click(0), m_pos(), + m_vel(), + m_drag(), m_size(), + m_closing(false), m_mouse_pos(), m_mouse_over(false), m_mouse_over_sym1(false), @@ -49,12 +62,12 @@ Notification::Notification(std::string id, bool no_auto_hide, bool no_auto_disab { if (is_disabled(id)) // The notification exists in the config as disabled. { - log_warning << "Requested launch of disabled notification with ID \"" << m_id << "\". Closing." << std::endl; + log_debug << "Requested launch of disabled notification with ID \"" << m_id << "\". Closing." << std::endl; m_quit = true; return; } - set_mini_text(_("Click for more details.")); // Set default mini text. + m_idle_close_timer.start(m_idle_close_time); } Notification::~Notification() @@ -84,21 +97,53 @@ Notification::set_mini_text(const std::string& text) void Notification::calculate_size() { + float mini_text_height = m_mini_text.empty() ? 0.f : m_mini_text_size.height + 24.f; m_size = Sizef(std::max(m_text_size.width, m_mini_text_size.width) + 60.0f, - m_text_size.height + m_mini_text_size.height + 40.0f); + m_text_size.height + mini_text_height + 16.f); + m_drag.x -= m_size.width + 100.f; } void Notification::draw(DrawingContext& context) { - if (m_quit || !MenuManager::instance().is_active()) // Close notification, if a quit has been requested, or the MenuManager isn't active. - { + // Close notification, if a quit has been requested, or neither the MenuManager or Editor aren't active. + if (m_quit || !(MenuManager::instance().is_active() || Editor::is_active())) close(); - return; + + if (m_alpha < 1.f || m_idle_close_timer.check()) + { + m_alpha -= 0.01f; + if (m_alpha <= 0.f) + close(); } + if (m_closing) + { + m_vel -= 0.8; + m_drag += m_vel; + + if (m_drag.x < -400 || m_drag.x > 0) + { + if (MouseCursor::current() && m_mouse_over) + MouseCursor::current()->set_state(MouseCursorState::NORMAL); + + MenuManager::instance().set_notification({}); + return; + } + } + else if (!m_mouse_down) + { + m_drag = m_drag * 0.8; + } + + context.push_transform(); + context.set_alpha(m_alpha); + m_pos = Vector(context.get_width() - std::max(m_text_size.width, m_mini_text_size.width) - 90.0f, static_cast(context.get_height() / 12) - m_text_size.height - m_mini_text_size.height + 10.0f); + m_pos.x -= m_drag.x; + float visibility = std::clamp(1.2f - (m_drag.x * 0.01f), 0.0f, 1.0f); + context.set_alpha(visibility); Rectf bg_rect(m_pos, m_size); // Draw background rect @@ -127,8 +172,8 @@ Notification::draw(DrawingContext& context) // Draw "Do not show again" and "Close" symbols, if the mouse is hovering over the notification. if (!m_mouse_over) return; - const std::string sym1 = "-"; - const std::string sym2 = "X"; + static const std::string sym1 = "-"; + static const std::string sym2 = "X"; Vector sym1_pos = Vector(bg_rect.get_left() + 5.0f, bg_rect.get_top()); Vector sym2_pos = Vector(bg_rect.get_right() - 15.0f, bg_rect.get_top()); @@ -159,6 +204,14 @@ Notification::draw(DrawingContext& context) m_mouse_pos.y + 20.0f), ALIGN_RIGHT, LAYER_GUI + 1, Color::CYAN); } + + context.pop_transform(); +} + +Vector +Notification::drag_amount(const SDL_Event& ev) const +{ + return m_init_mouse_click - VideoSystem::current()->get_viewport().to_logical(ev.button.x, ev.button.y); } void @@ -184,12 +237,36 @@ Notification::event(const SDL_Event& ev) close(); } else // Notification clicked (execute callback) + { + if (m_init_mouse_click.x == 0 && m_init_mouse_click.y == 0) + { + m_init_mouse_click = VideoSystem::current()->get_viewport().to_logical(ev.button.x, ev.button.y); + m_mouse_down = true; + } + } + } + } + break; + + case SDL_MOUSEBUTTONUP: + if (ev.button.button == SDL_BUTTON_LEFT) + { + m_init_mouse_click -= VideoSystem::current()->get_viewport().to_logical(ev.button.x, ev.button.y); + if (m_mouse_over) + { + if (std::abs(m_init_mouse_click.x) < DRAG_DEADZONE) { m_callback(); if (m_auto_disable) disable(); - if (m_auto_hide) close(); + if (m_auto_close) close(); + } + else if (std::abs(m_init_mouse_click.x) > DRAG_MAX) + { + close(); } } + m_init_mouse_click.x = m_init_mouse_click.y = 0; + m_mouse_down = false; } break; @@ -197,7 +274,24 @@ Notification::event(const SDL_Event& ev) { m_mouse_pos = VideoSystem::current()->get_viewport().to_logical(ev.motion.x, ev.motion.y); m_mouse_over = bg_rect.contains(m_mouse_pos); - if (MouseCursor::current() && m_mouse_over) MouseCursor::current()->set_state(MouseCursorState::LINK); + + if (m_mouse_over) + { + m_alpha = 1.f; + m_idle_close_timer.stop(); + } + else if (!m_idle_close_timer.started()) + { + m_idle_close_timer.start(m_idle_close_time); + } + + if (m_init_mouse_click.x != 0 && m_init_mouse_click.y != 0) + { + m_drag = drag_amount(ev); + } + + if (MouseCursor::current() && m_mouse_over) + MouseCursor::current()->set_state(MouseCursorState::LINK); } break; @@ -238,14 +332,13 @@ Notification::disable() void Notification::close() { - if (MouseCursor::current() && m_mouse_over) MouseCursor::current()->set_state(MouseCursorState::NORMAL); - MenuManager::instance().set_notification({}); + m_closing = true; } // Static functions, serving as utilities bool -Notification::is_disabled(std::string id) // Check if a notification is disabled by its ID. +Notification::is_disabled(const std::string& id) // Check if a notification is disabled by its ID. { return std::any_of(g_config->notifications.begin(), g_config->notifications.end(), [id](const auto& notif) diff --git a/src/gui/notification.hpp b/src/gui/notification.hpp index d40fe42356..6d4e5c7e4a 100644 --- a/src/gui/notification.hpp +++ b/src/gui/notification.hpp @@ -22,15 +22,20 @@ #include "control/controller.hpp" #include "math/sizef.hpp" +#include "supertux/timer.hpp" #include "video/drawing_context.hpp" class Notification { private: const std::string m_id; - const bool m_auto_hide; + const float m_idle_close_time; + const bool m_auto_close; const bool m_auto_disable; + Timer m_idle_close_timer; + float m_alpha; + std::string m_text; std::string m_mini_text; Sizef m_text_size; @@ -39,17 +44,25 @@ class Notification Vector m_pos; Sizef m_size; + Vector m_init_mouse_click; Vector m_mouse_pos; + Vector m_drag; + float m_vel; + bool m_dragging; + bool m_mouse_down; bool m_mouse_over; bool m_mouse_over_sym1; // Mouse is over "Do not show again". bool m_mouse_over_sym2; // Mouse is over "Close". + bool m_closing; + bool m_quit; // Requested notification quit. std::function m_callback; public: - Notification(std::string id, bool no_auto_hide = false, bool no_auto_disable = false); + Notification(const std::string& id, float idle_close_time = 0.f, + bool no_auto_close = false, bool auto_disable = false); ~Notification(); void set_text(const std::string& text); @@ -61,17 +74,17 @@ class Notification void draw(DrawingContext& context); // Notification actions - void disable(); void close(); // Static functions, serving as utilities - - static bool is_disabled(std::string id); + static bool is_disabled(const std::string& id); private: void calculate_size(); + Vector drag_amount(const SDL_Event& ev) const; + private: Notification(const Notification&) = delete; Notification& operator=(const Notification&) = delete; diff --git a/src/interface/control.cpp b/src/interface/control.cpp index ab69962a89..6627acf255 100644 --- a/src/interface/control.cpp +++ b/src/interface/control.cpp @@ -17,10 +17,25 @@ #include "interface/control.hpp" InterfaceControl::InterfaceControl() : - m_on_change(), + m_on_activate_callbacks(), + m_on_change_callbacks(), m_label(), m_has_focus(), m_rect(), m_parent(nullptr) { } + +void +InterfaceControl::call_on_activate_callbacks() const +{ + for (const auto& on_activate : m_on_activate_callbacks) + on_activate(); +} + +void +InterfaceControl::call_on_change_callbacks() const +{ + for (const auto& on_change : m_on_change_callbacks) + on_change(); +} diff --git a/src/interface/control.hpp b/src/interface/control.hpp index 9a79f03402..4c6614b465 100644 --- a/src/interface/control.hpp +++ b/src/interface/control.hpp @@ -30,8 +30,22 @@ class InterfaceControl : public Widget InterfaceControl(); ~InterfaceControl() override {} - virtual void draw(DrawingContext& context) override { if (m_label) m_label->draw(context); } - virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override { if (m_label) m_label->on_mouse_motion(motion); return false; } + virtual void draw(DrawingContext& context) override + { + if (m_label) + { + m_label->draw(context); + } + } + + virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override + { + if (m_label) + { + m_label->on_mouse_motion(motion); + } + return false; + } inline void set_focus(bool focus) { m_has_focus = focus; } inline bool has_focus() const { return m_has_focus; } @@ -39,11 +53,19 @@ class InterfaceControl : public Widget inline void set_rect(const Rectf& rect) { m_rect = rect; } inline Rectf get_rect() const { return m_rect; } +protected: + void call_on_activate_callbacks() const; + void call_on_change_callbacks() const; + public: + /** Optional; a function that will be called each time the control is activated. + */ + std::vector> m_on_activate_callbacks; + /** Optional; a function that will be called each time the bound value * is modified. */ - std::function m_on_change; + std::vector> m_on_change_callbacks; /** Optional; the label associated with the control */ std::unique_ptr m_label; diff --git a/src/interface/control_button.cpp b/src/interface/control_button.cpp index 6acd889731..52c06ef1b8 100644 --- a/src/interface/control_button.cpp +++ b/src/interface/control_button.cpp @@ -37,13 +37,13 @@ ControlButton::draw(DrawingContext& context) (m_has_focus ? Color(0.75f, 0.75f, 0.7f, 1.f) : Color(0.5f, 0.5f, 0.5f, 1.f)), LAYER_GUI); - context.color().draw_text(Resources::control_font, + context.color().draw_text(Resources::small_font, m_btn_label, Vector((m_rect.get_left() + m_rect.get_right()) / 2, - (m_rect.get_top() + m_rect.get_bottom()) / 2 - Resources::control_font->get_height() / 2), + (m_rect.get_top() + m_rect.get_bottom()) / 2 - Resources::small_font->get_height() / 2), FontAlignment::ALIGN_CENTER, LAYER_GUI, - Color::BLACK); + Color::WHITE); } bool @@ -60,10 +60,9 @@ ControlButton::on_mouse_button_up(const SDL_MouseButtonEvent& button) m_mouse_down = false; - if (m_on_change) - m_on_change(); + call_on_activate_callbacks(); - m_has_focus = true; + m_has_focus = false; return true; } @@ -90,8 +89,7 @@ ControlButton::on_key_up(const SDL_KeyboardEvent& key) return false; if (key.keysym.sym == SDLK_SPACE) { - if (m_on_change) - m_on_change(); + call_on_activate_callbacks(); m_mouse_down = false; return true; } diff --git a/src/interface/control_checkbox.cpp b/src/interface/control_checkbox.cpp index 518c4bfdd3..fa9c1ff4c1 100644 --- a/src/interface/control_checkbox.cpp +++ b/src/interface/control_checkbox.cpp @@ -35,13 +35,13 @@ ControlCheckbox::draw(DrawingContext& context) m_has_focus ? Color(0.75f, 0.75f, 0.7f, 1.f) : Color(0.5f, 0.5f, 0.5f, 1.f), LAYER_GUI); if (*m_value) { - context.color().draw_text(Resources::control_font, + context.color().draw_text(Resources::small_font, "X", Vector((m_rect.get_left() + m_rect.get_right()) / 2 + 1.f, - (m_rect.get_top() + m_rect.get_bottom()) / 2 - Resources::control_font->get_height() / 2), + (m_rect.get_top() + m_rect.get_bottom()) / 2 - Resources::small_font->get_height() / 2), FontAlignment::ALIGN_CENTER, LAYER_GUI, - Color::BLACK); + Color::WHITE); } } @@ -56,10 +56,11 @@ ControlCheckbox::on_mouse_button_up(const SDL_MouseButtonEvent& button) if (!m_rect.contains(mouse_pos)) return false; + call_on_activate_callbacks(); + *m_value = !*m_value; - if (m_on_change) - m_on_change(); + call_on_change_callbacks(); m_has_focus = true; @@ -82,10 +83,10 @@ ControlCheckbox::on_key_up(const SDL_KeyboardEvent& key) if (key.keysym.sym != SDLK_SPACE || !m_has_focus) return false; - *m_value = !*m_value; + call_on_activate_callbacks(); - if (m_on_change) - m_on_change(); + *m_value = !*m_value; + call_on_change_callbacks(); return true; } diff --git a/src/interface/control_enum.hpp b/src/interface/control_enum.hpp index 97fca3a223..733760ee03 100644 --- a/src/interface/control_enum.hpp +++ b/src/interface/control_enum.hpp @@ -107,14 +107,14 @@ ControlEnum::draw(DrawingContext& context) label = ""; } - context.color().draw_text(Resources::control_font, + context.color().draw_text(Resources::small_font, label, Vector(m_rect.get_left() + 5.f, (m_rect.get_top() + m_rect.get_bottom()) / 2 - - Resources::control_font->get_height() / 2), + Resources::small_font->get_height() / 2), FontAlignment::ALIGN_LEFT, LAYER_GUI + 1, - Color::BLACK); + Color::WHITE); int i = 0; if (m_open_list) { for (const auto& option : m_options) { @@ -132,15 +132,15 @@ ControlEnum::draw(DrawingContext& context) std::string label2 = option.second; - context.color().draw_text(Resources::control_font, + context.color().draw_text(Resources::small_font, label2, Vector(m_rect.get_left() + 5.f, (m_rect.get_top() + m_rect.get_bottom()) / 2 - - Resources::control_font->get_height() / 2 + + Resources::small_font->get_height() / 2 + m_rect.get_height() * float(i)), FontAlignment::ALIGN_LEFT, LAYER_GUI + 6, - Color::BLACK); + Color::WHITE); } } } @@ -156,6 +156,8 @@ ControlEnum::on_mouse_button_up(const SDL_MouseButtonEvent& button) if (m_rect.contains(mouse_pos)) { m_open_list = !m_open_list; m_has_focus = true; + if (m_open_list) + call_on_activate_callbacks(); return true; } else if (get_list_rect().contains(mouse_pos) && m_open_list) { return true; @@ -184,11 +186,12 @@ ControlEnum::on_mouse_button_down(const SDL_MouseButtonEvent& button) if (--pos != -1) continue; *m_value = option.first; - if (m_on_change) - m_on_change(); + call_on_change_callbacks(); break; } + m_has_focus = false; + m_open_list = false; } else { log_warning << "Clicked on control enum inside dropdown but at invalid position (" << pos << " for a size of " << m_options.size() << ")" << std::endl; @@ -223,6 +226,8 @@ ControlEnum::on_key_up(const SDL_KeyboardEvent& key) || key.keysym.sym == SDLK_RETURN || key.keysym.sym == SDLK_RETURN2) && m_has_focus) { m_open_list = !m_open_list; + if (m_open_list) + call_on_activate_callbacks(); return true; } else { return false; @@ -253,9 +258,7 @@ ControlEnum::on_key_down(const SDL_KeyboardEvent& key) if (is_next && !m_options.empty()) *m_value = m_options.begin()->first; - if (m_on_change) - m_on_change(); - + call_on_change_callbacks(); return true; } else if (key.keysym.sym == SDLK_UP) { @@ -279,9 +282,7 @@ ControlEnum::on_key_down(const SDL_KeyboardEvent& key) if (is_last) *m_value = last_value; - if (m_on_change) - m_on_change(); - + call_on_change_callbacks(); return true; } diff --git a/src/interface/control_textbox.cpp b/src/interface/control_textbox.cpp index 89c94116f5..c7d1d15468 100644 --- a/src/interface/control_textbox.cpp +++ b/src/interface/control_textbox.cpp @@ -84,13 +84,13 @@ ControlTextbox::draw(DrawingContext& context) LAYER_GUI); if (m_caret_pos != m_secondary_caret_pos) { - float lgt1 = Resources::control_font + float lgt1 = Resources::small_font ->get_text_width(get_first_chars_visible(std::max( std::min(m_caret_pos, m_secondary_caret_pos) - m_current_offset, 0 ))); - float lgt2 = Resources::control_font + float lgt2 = Resources::small_font ->get_text_width(get_first_chars_visible(std::min( std::max(m_caret_pos, m_secondary_caret_pos) - m_current_offset, int(get_contents_visible().size()) @@ -104,21 +104,21 @@ ControlTextbox::draw(DrawingContext& context) LAYER_GUI); } - context.color().draw_text(Resources::control_font, + context.color().draw_text(Resources::small_font, get_contents_visible(), Vector(m_rect.get_left() + 5.f, (m_rect.get_top() + m_rect.get_bottom()) / 2 - - Resources::control_font->get_height() / 2), + Resources::small_font->get_height() / 2), FontAlignment::ALIGN_LEFT, LAYER_GUI + 1, - Color::BLACK); + Color::WHITE); if (m_cursor_timer > 0 && m_has_focus) { - float lgt = Resources::control_font + float lgt = Resources::small_font ->get_text_width(get_first_chars_visible(m_caret_pos - m_current_offset)); context.color().draw_line(m_rect.p1() + Vector(lgt + 5.f, 2.f), m_rect.p1() + Vector(lgt + 5.f, - Resources::control_font->get_height() + 4.f), + Resources::small_font->get_height() + 4.f), Color::BLACK, LAYER_GUI + 1); } @@ -134,6 +134,7 @@ ControlTextbox::on_mouse_button_down(const SDL_MouseButtonEvent& button) m_caret_pos = get_text_position(mouse_pos); m_secondary_caret_pos = m_caret_pos; m_mouse_pressed = true; + call_on_activate_callbacks(); return true; } else { if (m_has_focus) { @@ -171,7 +172,6 @@ ControlTextbox::on_mouse_motion(const SDL_MouseMotionEvent& motion) bool ControlTextbox::on_key_up(const SDL_KeyboardEvent& key) { - if (m_has_focus) { if (key.keysym.sym == SDLK_LSHIFT || key.keysym.sym == SDLK_RSHIFT) @@ -295,6 +295,7 @@ ControlTextbox::on_key_down(const SDL_KeyboardEvent& key) } else if (key.keysym.sym == SDLK_RETURN) { + m_has_focus = false; parse_value(); return true; } @@ -315,24 +316,25 @@ ControlTextbox::event(const SDL_Event& ev) { bool ControlTextbox::parse_value(bool call_on_change /* = true (see header)*/) { + std::string new_str = get_contents(); + // Abort if we have a validation function for the string, and the function // says the string is invalid. if (m_validate_string) { - if (!m_validate_string(this, get_contents())) { + if (!m_validate_string(this, new_str)) { revert_value(); return false; } } - std::string new_str = get_string(); if (m_internal_string_backup != new_str) { m_internal_string_backup = new_str; if (m_string) *m_string = new_str; - if (call_on_change && m_on_change) - m_on_change(); + if (call_on_change) + call_on_change_callbacks(); } return true; @@ -412,7 +414,7 @@ ControlTextbox::get_text_position(const Vector& pos) const float dist = pos.x - m_rect.get_left(); int i = 0; - while (Resources::control_font->get_text_width(get_first_chars_visible(i)) < dist + while (Resources::small_font->get_text_width(get_first_chars_visible(i)) < dist && i <= int(m_charlist.size())) i++; @@ -434,21 +436,24 @@ ControlTextbox::get_truncated_text(const std::string& text) const bool ControlTextbox::fits(const std::string& text) const { - return Resources::control_font->get_text_width(text) <= m_rect.get_width() - 10.f; + return Resources::small_font->get_text_width(text) <= m_rect.get_width() - 10.f; } void ControlTextbox::recenter_offset() { + auto contents = get_contents(); + auto visible_contents = get_contents_visible(); + while (m_caret_pos < m_current_offset && m_current_offset > 0) { m_current_offset--; } - while (m_caret_pos > m_current_offset + int(get_contents_visible().size()) && m_current_offset < int(get_contents().size())) { + while (m_caret_pos > m_current_offset + int(visible_contents.size()) && m_current_offset < int(contents.size())) { m_current_offset++; } - while (m_current_offset > 0 && fits(get_contents().substr(m_current_offset - 1))) { + while (m_current_offset > 0 && fits(contents.substr(m_current_offset - 1))) { m_current_offset--; } } diff --git a/src/interface/control_textbox.hpp b/src/interface/control_textbox.hpp index 7cccb722a0..2edf3d61f6 100644 --- a/src/interface/control_textbox.hpp +++ b/src/interface/control_textbox.hpp @@ -38,7 +38,16 @@ class ControlTextbox : public InterfaceControl virtual void update(float dt_sec) override; /** Binds a string to the textbox */ - inline void bind_string(std::string* value) { m_string = value; } + inline void bind_string(std::string* value) + { + m_string = value; + + if(value != nullptr) + { + m_internal_string_backup = *value; + revert_value(); + } + } /** Returns the full string held in m_charlist */ const std::string& get_string() const; diff --git a/src/interface/control_textbox_float.cpp b/src/interface/control_textbox_float.cpp index 4557461cba..8e0d38b1b0 100644 --- a/src/interface/control_textbox_float.cpp +++ b/src/interface/control_textbox_float.cpp @@ -20,6 +20,7 @@ #include ControlTextboxFloat::ControlTextboxFloat() : + ControlTextbox(), m_validate_float(), m_value(nullptr) { @@ -70,8 +71,8 @@ ControlTextboxFloat::parse_value(bool call_on_change /* = true (see header */) // Revert the value regardless. revert_value(); - if (call_on_change && m_on_change) - m_on_change(); + if (call_on_change) + call_on_change_callbacks(); } return true; diff --git a/src/interface/control_textbox_int.cpp b/src/interface/control_textbox_int.cpp index b79dd27cde..fda0aa9742 100644 --- a/src/interface/control_textbox_int.cpp +++ b/src/interface/control_textbox_int.cpp @@ -20,6 +20,7 @@ #include ControlTextboxInt::ControlTextboxInt() : + ControlTextbox(), m_validate_int(), m_value(nullptr) { @@ -70,8 +71,8 @@ ControlTextboxInt::parse_value(bool call_on_change /* = true (see header */) // Revert the value regardless. revert_value(); - if (call_on_change && m_on_change) - m_on_change(); + if (call_on_change) + call_on_change_callbacks(); } return true; diff --git a/src/interface/label.cpp b/src/interface/label.cpp index 6f5ae038e8..700f49e6cf 100644 --- a/src/interface/label.cpp +++ b/src/interface/label.cpp @@ -23,6 +23,7 @@ InterfaceLabel::InterfaceLabel() : m_rect(), m_label(), + m_description(), m_mouse_pos(0.0f, 0.0f) { } @@ -30,6 +31,15 @@ InterfaceLabel::InterfaceLabel() : InterfaceLabel::InterfaceLabel(const Rectf& rect, std::string label) : m_rect(rect), m_label(std::move(label)), + m_description(), + m_mouse_pos(0.0f, 0.0f) +{ +} + +InterfaceLabel::InterfaceLabel(const Rectf& rect, std::string label, std::string description) : + m_rect(rect), + m_label(std::move(label)), + m_description(std::move(description)), m_mouse_pos(0.0f, 0.0f) { } @@ -44,43 +54,55 @@ InterfaceLabel::on_mouse_motion(const SDL_MouseMotionEvent& motion) void InterfaceLabel::draw(DrawingContext& context) { - context.color().draw_text(Resources::control_font, + context.color().draw_text(Resources::small_font, get_truncated_text(), Vector(m_rect.get_left() + 5.f, (m_rect.get_top() + m_rect.get_bottom()) / 2 - - Resources::control_font->get_height() / 2 + 1.f), + Resources::small_font->get_height() / 2 + 1.f), FontAlignment::ALIGN_LEFT, LAYER_GUI, Color::WHITE); - if (!fits(m_label) && m_rect.contains(m_mouse_pos)) { - context.color().draw_filled_rect(Rectf(m_mouse_pos, m_mouse_pos + Vector( - Resources::control_font - ->get_text_width(m_label), - Resources::control_font->get_height())) - .grown(5.f).moved(Vector(0, 32)), - Color(0.1f, 0.1f, 0.1f, 0.8f), - LAYER_GUI + 10); - context.color().draw_filled_rect(Rectf(m_mouse_pos, m_mouse_pos + Vector( - Resources::control_font - ->get_text_width(m_label), - Resources::control_font->get_height())) - .grown(3.f).moved(Vector(0, 32)), - Color(1.f, 1.f, 1.f, 0.1f), - LAYER_GUI + 10); - context.color().draw_text(Resources::control_font, - m_label, + auto has_description = m_description.length() > 0; + if ((!fits(m_label) || has_description) && m_rect.contains(m_mouse_pos)) { + auto font = Resources::small_font; + auto text_width = font->get_text_width(m_label); + auto text_height = font->get_height() * (has_description ? 2 : 1); + + if (has_description) + { + auto description_width = font->get_text_width(m_description); + if (description_width > text_width) + text_width = description_width; + } + + auto base_rect = Rectf(m_mouse_pos, m_mouse_pos + Vector(text_width, text_height)); + auto box_layer = LAYER_GUI + 10; + + context.color().draw_filled_rect(base_rect.grown(5.f).moved(Vector(0, 32)), + Color(0.1f, 0.1f, 0.1f, 0.8f), box_layer); + + context.color().draw_filled_rect(base_rect.grown(3.f).moved(Vector(0, 32)), + Color(1.f, 1.f, 1.f, 0.1f), box_layer); + + context.color().draw_text(font, m_label, m_mouse_pos + Vector(0, 33.f), - FontAlignment::ALIGN_LEFT, - LAYER_GUI + 11, + FontAlignment::ALIGN_LEFT, LAYER_GUI + 11, Color::WHITE); + if (has_description) + { + context.color().draw_text(font, m_description, + m_mouse_pos + Vector(0, 33.f + font->get_height() + 2.5f), + FontAlignment::ALIGN_LEFT, LAYER_GUI + 11, + Color::YELLOW); + } } } bool InterfaceLabel::fits(const std::string& text) const { - return Resources::control_font->get_text_width(text) <= m_rect.get_width(); + return Resources::small_font->get_text_width(text) <= m_rect.get_width(); } std::string diff --git a/src/interface/label.hpp b/src/interface/label.hpp index e8c5133a13..b7815d26a7 100644 --- a/src/interface/label.hpp +++ b/src/interface/label.hpp @@ -26,6 +26,7 @@ class InterfaceLabel : public Widget public: InterfaceLabel(); InterfaceLabel(const Rectf& rect, std::string label); + InterfaceLabel(const Rectf& rect, std::string label, std::string description); ~InterfaceLabel() override {} virtual void draw(DrawingContext& context) override; @@ -37,6 +38,9 @@ class InterfaceLabel : public Widget inline void set_label(const std::string& label) { m_label = label; } inline const std::string& get_label() const { return m_label; } + inline void set_description(const std::string& description) { m_description = description; } + inline const std::string& get_description() const { return m_description; } + bool fits(const std::string& text) const; std::string get_truncated_text() const; @@ -45,6 +49,8 @@ class InterfaceLabel : public Widget Rectf m_rect; /** The text of the label */ std::string m_label; + /** Some descriptive text for the label */ + std::string m_description; private: Vector m_mouse_pos; diff --git a/src/math/rectf.hpp b/src/math/rectf.hpp index 8a5af8b88e..09d2b6faa0 100644 --- a/src/math/rectf.hpp +++ b/src/math/rectf.hpp @@ -50,15 +50,11 @@ class Rectf final Rectf(const Vector& np1, const Vector& np2) : m_p1(np1), m_size(np2.x - np1.x, np2.y - np1.y) { - assert(m_size.width >= 0 && - m_size.height >= 0); } Rectf(float x1, float y1, float x2, float y2) : m_p1(x1, y1), m_size(x2 - x1, y2 - y1) { - assert(m_size.width >= 0 && - m_size.height >= 0); } Rectf(const Vector& p1, const Sizef& size) : diff --git a/src/object/background.cpp b/src/object/background.cpp index 2545e6400c..bd13752b6d 100644 --- a/src/object/background.cpp +++ b/src/object/background.cpp @@ -279,8 +279,8 @@ Background::get_settings() { ObjectSettings result = GameObject::get_settings(); - result.add_float(_("X"), &m_pos.x, "x", 0.0f, OPTION_HIDDEN); - result.add_float(_("Y"), &m_pos.y, "y", 0.0f, OPTION_HIDDEN); + result.add_float(_("X"), &m_pos.x, "x", 0.0f, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); + result.add_float(_("Y"), &m_pos.y, "y", 0.0f, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); result.add_bool(_("Fill"), &m_fill, "fill", false); result.add_int(_("Z-pos"), &m_layer, "z-pos", LAYER_BACKGROUND0); diff --git a/src/object/bicycle_platform.cpp b/src/object/bicycle_platform.cpp index 1ec458f856..22587300f9 100644 --- a/src/object/bicycle_platform.cpp +++ b/src/object/bicycle_platform.cpp @@ -198,8 +198,8 @@ BicyclePlatform::get_settings() { auto result = GameObject::get_settings(); - result.add_float(_("X"), &m_center.x, "x", 0.0f, OPTION_HIDDEN); - result.add_float(_("Y"), &m_center.y, "y", 0.0f, OPTION_HIDDEN); + result.add_float(_("X"), &m_center.x, "x", 0.0f, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); + result.add_float(_("Y"), &m_center.y, "y", 0.0f, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); result.add_int(_("Platforms"), &m_platforms, "platforms", 2); result.add_float(_("Radius"), &m_radius, "radius", 128.0f); diff --git a/src/object/bonus_block.cpp b/src/object/bonus_block.cpp index 273a6f594e..ca722bc47d 100644 --- a/src/object/bonus_block.cpp +++ b/src/object/bonus_block.cpp @@ -261,7 +261,7 @@ BonusBlock::get_settings() { ObjectSettings result = Block::get_settings(); - result.add_script(_("Script"), &m_script, "script"); + result.add_script(get_uid(), _("Script"), &m_script, "script"); result.add_int(_("Count"), &m_hit_counter, "count", get_default_hit_counter()); result.add_enum(_("Content"), reinterpret_cast(&m_contents), { _("Coin"), _("Growth (fire flower)"), _("Growth (ice flower)"), _("Growth (air flower)"), diff --git a/src/object/coin.cpp b/src/object/coin.cpp index 16657398a0..8c7db8bea4 100644 --- a/src/object/coin.cpp +++ b/src/object/coin.cpp @@ -335,7 +335,7 @@ Coin::get_settings() result.add_path_handle(_("Handle"), m_path_handle, "handle"); } - result.add_script(_("Collect script"), &m_collect_script, "collect-script"); + result.add_script(get_uid(), _("Collect script"), &m_collect_script, "collect-script"); result.reorder({"collect-script", "path-ref"}); @@ -371,7 +371,7 @@ HeavyCoin::get_settings() { auto result = MovingSprite::get_settings(); - result.add_script(_("Collect script"), &m_collect_script, "collect-script"); + result.add_script(get_uid(), _("Collect script"), &m_collect_script, "collect-script"); result.reorder({"collect-script", "sprite", "x", "y"}); diff --git a/src/object/firefly.cpp b/src/object/firefly.cpp index 7c10ba1ebb..c9324e748f 100644 --- a/src/object/firefly.cpp +++ b/src/object/firefly.cpp @@ -141,7 +141,7 @@ ObjectSettings Firefly::get_settings() { ObjectSettings result = MovingSprite::get_settings(); - result.add_test_from_here(); + result.add_test_from_here(this); return result; } diff --git a/src/object/infoblock.cpp b/src/object/infoblock.cpp index b2a0520503..4e1f8699c4 100644 --- a/src/object/infoblock.cpp +++ b/src/object/infoblock.cpp @@ -73,7 +73,7 @@ InfoBlock::get_settings() { ObjectSettings result = Block::get_settings(); - result.add_multiline_translatable_text(_("Message"), &m_message, "message"); + result.add_multiline_translatable_text(get_uid(), _("Message"), &m_message, "message"); result.add_color(_("Front Color"), &m_frontcolor, "frontcolor", Color(0.6f, 0.7f, 0.8f, 0.5f)); diff --git a/src/object/ispy.cpp b/src/object/ispy.cpp index 54f750843f..96eab771fd 100644 --- a/src/object/ispy.cpp +++ b/src/object/ispy.cpp @@ -52,7 +52,7 @@ Ispy::get_settings() { ObjectSettings result = StickyObject::get_settings(); - result.add_script(_("Script"), &m_script, "script"); + result.add_script(get_uid(), _("Script"), &m_script, "script"); result.add_direction(_("Direction"), &m_dir, { Direction::LEFT, Direction::RIGHT, Direction::UP, Direction::DOWN }, "direction"); diff --git a/src/object/moving_sprite.cpp b/src/object/moving_sprite.cpp index 7ae025e8c1..bd1edfabc3 100644 --- a/src/object/moving_sprite.cpp +++ b/src/object/moving_sprite.cpp @@ -198,8 +198,10 @@ MovingSprite::get_settings() { ObjectSettings result = MovingObject::get_settings(); - result.add_sprite(_("Sprite"), &m_sprite_name, "sprite", get_default_sprite_name()); - result.add_int(_("Z-pos"), &m_layer, "z-pos"); + result.add_sprite(_("Sprite"), &m_sprite_name, "sprite", get_default_sprite_name()) + ->set_description(_("The sprite file used for this object.")); + result.add_int(_("Z-pos"), &m_layer, "z-pos") + ->set_description(_("The layer this object is drawn on. Higher layers are drawn on top of lower layers.")); result.reorder({"sprite", "z-pos", "x", "y"}); diff --git a/src/object/pneumatic_platform.cpp b/src/object/pneumatic_platform.cpp index 7fc0da773c..1c704555e4 100644 --- a/src/object/pneumatic_platform.cpp +++ b/src/object/pneumatic_platform.cpp @@ -157,8 +157,8 @@ PneumaticPlatform::get_settings() ObjectSettings result = GameObject::get_settings(); result.add_sprite(_("Sprite"), &m_sprite_name, "sprite", "images/objects/platforms/small.sprite"); - result.add_float(_("X"), &m_pos.x, "x", 0.0f, OPTION_HIDDEN); - result.add_float(_("Y"), &m_pos.y, "y", 0.0f, OPTION_HIDDEN); + result.add_float(_("X"), &m_pos.x, "x", 0.0f, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); + result.add_float(_("Y"), &m_pos.y, "y", 0.0f, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); return result; } diff --git a/src/object/powerup.cpp b/src/object/powerup.cpp index 54480880a5..5758a93a59 100644 --- a/src/object/powerup.cpp +++ b/src/object/powerup.cpp @@ -323,7 +323,7 @@ PowerUp::get_settings() { ObjectSettings result = MovingSprite::get_settings(); - result.add_script(_("Script"), &script, "script"); + result.add_script(get_uid(), _("Script"), &script, "script"); result.add_bool(_("Disable gravity"), &no_physics, "disable-physics", false); result.reorder({"script", "disable-physics", "sprite", "x", "y"}); diff --git a/src/object/pushbutton.cpp b/src/object/pushbutton.cpp index b24f92b65f..324a71db65 100644 --- a/src/object/pushbutton.cpp +++ b/src/object/pushbutton.cpp @@ -63,7 +63,7 @@ PushButton::get_settings() ObjectSettings result = StickyObject::get_settings(); result.add_direction(_("Direction"), &m_dir, { Direction::UP, Direction::DOWN }, "direction"); - result.add_script(_("Script"), &m_script, "script"); + result.add_script(get_uid(), _("Script"), &m_script, "script"); result.reorder({"direction", "script", "sticky", "x", "y"}); diff --git a/src/object/rock.cpp b/src/object/rock.cpp index 362315ca4c..1fae2d6451 100644 --- a/src/object/rock.cpp +++ b/src/object/rock.cpp @@ -313,8 +313,8 @@ ObjectSettings Rock::get_settings() { auto result = MovingSprite::get_settings(); - result.add_script(_("On-grab script"), &m_on_grab_script, "on-grab-script"); - result.add_script(_("On-ungrab script"), &m_on_ungrab_script, "on-ungrab-script"); + result.add_script(get_uid(), _("On-grab script"), &m_on_grab_script, "on-grab-script"); + result.add_script(get_uid(), _("On-ungrab script"), &m_on_ungrab_script, "on-ungrab-script"); return result; } diff --git a/src/object/scripted_object.cpp b/src/object/scripted_object.cpp index 2d91111f15..b6ab66d37f 100644 --- a/src/object/scripted_object.cpp +++ b/src/object/scripted_object.cpp @@ -67,12 +67,12 @@ ScriptedObject::get_settings() ObjectSettings result = MovingSprite::get_settings(); - //result.add_float("width", &new_size.x, "width", OPTION_HIDDEN); - //result.add_float("height", &new_size.y, "height", OPTION_HIDDEN); + //result.add_float("width", &new_size.x, "width", OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); + //result.add_float("height", &new_size.y, "height", OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); result.add_bool(_("Solid"), &solid, "solid", true); result.add_bool(_("Physics enabled"), &physic_enabled, "physic-enabled", true); result.add_bool(_("Visible"), &visible, "visible", true); - result.add_script(_("Hit script"), &hit_script, "hit-script"); + result.add_script(get_uid(), _("Hit script"), &hit_script, "hit-script"); result.reorder({"z-pos", "visible", "physic-enabled", "solid", "name", "sprite", "script", "button", "x", "y"}); diff --git a/src/object/spawnpoint.cpp b/src/object/spawnpoint.cpp index 2e3b9b2d0b..aba891fb03 100644 --- a/src/object/spawnpoint.cpp +++ b/src/object/spawnpoint.cpp @@ -29,9 +29,7 @@ SpawnPointMarker::SpawnPointMarker(const std::string& name, const Vector& pos) : m_col.m_bbox.set_p1(pos); m_col.m_bbox.set_size(32, 32); - if (!Editor::is_active()) { - set_group(COLGROUP_DISABLED); - } + set_group(COLGROUP_DISABLED); } SpawnPointMarker::SpawnPointMarker(const ReaderMapping& mapping) : @@ -58,6 +56,6 @@ ObjectSettings SpawnPointMarker::get_settings() { ObjectSettings result = MovingObject::get_settings(); - result.add_test_from_here(); + result.add_test_from_here(this); return result; } diff --git a/src/object/textscroller.cpp b/src/object/textscroller.cpp index d9c9315cc9..e9a1118d62 100644 --- a/src/object/textscroller.cpp +++ b/src/object/textscroller.cpp @@ -341,7 +341,7 @@ TextScroller::get_settings() ObjectSettings result = GameObject::get_settings(); result.add_file(_("File"), &m_filename, "file"); - result.add_script(_("Finish Script"), &m_finish_script, "finish-script"); + result.add_script(get_uid(), _("Finish Script"), &m_finish_script, "finish-script"); result.add_float(_("Speed"), &m_default_speed, "speed", DEFAULT_SPEED); result.add_float(_("X-offset"), &m_x_offset, "x-offset"); result.add_bool(_("Controllable"), &m_controllable, "controllable", true); diff --git a/src/object/tilemap.cpp b/src/object/tilemap.cpp index 0b9f8d067e..a53dde685d 100644 --- a/src/object/tilemap.cpp +++ b/src/object/tilemap.cpp @@ -27,6 +27,7 @@ #include "supertux/debug.hpp" #include "supertux/gameconfig.hpp" #include "supertux/globals.hpp" +#include "supertux/level.hpp" #include "supertux/resources.hpp" #include "supertux/sector.hpp" #include "supertux/tile.hpp" @@ -53,7 +54,7 @@ TileMap::TileMap(const TileSet *new_tileset) : m_width(0), m_height(0), m_z_pos(0), - m_offset(Vector(0,0)), + m_offset(Vector(0, 0)), m_movement(0, 0), m_objects_hit_bottom(), m_ground_movement_manager(nullptr), @@ -393,7 +394,7 @@ TileMap::after_editor_set() } } else { if (m_add_path) { - init_path_pos(m_offset); + init_path_pos(get_offset()); } } @@ -677,15 +678,22 @@ TileMap::resize(int new_width, int new_height, int fill_id, apply_offset_y(fill_id, yoffset); } -void TileMap::resize(const Size& newsize, const Size& resize_offset) { +void +TileMap::resize(const Size& newsize, const Size& resize_offset) { resize(newsize.width, newsize.height, 0, resize_offset.width, resize_offset.height); } +Vector +TileMap::get_offset() const +{ + return m_offset; +} + Rect TileMap::get_tiles_overlapping(const Rectf &rect) const { Rectf rect2 = rect; - rect2.move(-m_offset); + rect2.move(-get_offset()); int t_left = std::max(0 , int(floorf(rect2.get_left () / 32))); int t_right = std::min(m_width , int(ceilf (rect2.get_right () / 32))); @@ -738,7 +746,7 @@ TileMap::get_tile_id(const Vector& pos) const bool TileMap::is_outside_bounds(const Vector& pos) const { - auto pos_ = (pos - m_offset) / 32.0f; + auto pos_ = (pos - get_offset()) / 32.0f; float width = static_cast(m_width); float height = static_cast(m_height); return pos_.x < 0 || pos_.x >= width || pos_.y < 0 || pos_.y >= height; @@ -754,7 +762,7 @@ TileMap::get_tile(int x, int y) const uint32_t TileMap::get_tile_id_at(const Vector& pos) const { - Vector xy = (pos - m_offset) / 32.0f; + Vector xy = (pos - get_offset()) / 32.0f; return get_tile_id(static_cast(xy.x), static_cast(xy.y)); } @@ -789,7 +797,7 @@ TileMap::change(int idx, uint32_t newtile) void TileMap::change_at(const Vector& pos, uint32_t newtile) { - Vector xy = (pos - m_offset) / 32.0f; + Vector xy = (pos - get_offset()) / 32.0f; change(int(xy.x), int(xy.y), newtile); } @@ -1021,11 +1029,11 @@ void TileMap::move_by(const Vector& shift) { if (!get_path()) { - init_path_pos(m_offset); + init_path_pos(get_offset()); m_add_path = true; } get_path()->move_by(shift); - m_offset += shift; + set_offset(get_offset() + shift); } void diff --git a/src/object/tilemap.hpp b/src/object/tilemap.hpp index 0c3edd69a9..5d54a79837 100644 --- a/src/object/tilemap.hpp +++ b/src/object/tilemap.hpp @@ -100,7 +100,7 @@ class TileMap final : public LayerObject, inline Size get_size() const { return Size(m_width, m_height); } inline void set_offset(const Vector &offset_) { m_offset = offset_; } - inline Vector get_offset() const { return m_offset; } + Vector get_offset() const; void set_ground_movement_manager(const std::shared_ptr& movement_manager) { @@ -125,7 +125,7 @@ class TileMap final : public LayerObject, /** Returns the position of the upper-left corner of tile (x, y) in the sector. */ Vector get_tile_position(int x, int y) const - { return m_offset + Vector(static_cast(x), static_cast(y)) * 32.0f; } + { return get_offset() + Vector(static_cast(x), static_cast(y)) * 32.0f; } Rectf get_bbox() const { return Rectf(get_tile_position(0, 0), diff --git a/src/sprite/sprite.cpp b/src/sprite/sprite.cpp index ba64529224..d8d25041d8 100644 --- a/src/sprite/sprite.cpp +++ b/src/sprite/sprite.cpp @@ -18,6 +18,7 @@ #include +#include "editor/editor.hpp" #include "supertux/direction.hpp" #include "supertux/globals.hpp" #include "util/log.hpp" @@ -138,6 +139,12 @@ Sprite::animation_done() const void Sprite::update() { + if (Editor::is_active() && !g_config->editor_render_animations) + { + m_frameidx = 0; + return; + } + float frame_inc = m_last_ticks > 0.f ? m_action->fps * (g_game_time - m_last_ticks) : 0.f; m_last_ticks = g_game_time; diff --git a/src/supertux/game_manager.cpp b/src/supertux/game_manager.cpp index f3c81fcb4e..2fe931409b 100644 --- a/src/supertux/game_manager.cpp +++ b/src/supertux/game_manager.cpp @@ -30,11 +30,15 @@ #include "util/reader.hpp" #include "util/reader_document.hpp" #include "util/reader_mapping.hpp" +#include "util/writer.hpp" #include "worldmap/tux.hpp" #include "worldmap/worldmap.hpp" +#include "supertux/game_session.hpp" +#include GameManager::GameManager() : - m_savegame() + m_savegame(), + m_levelstream() { } @@ -47,55 +51,84 @@ GameManager::save() void GameManager::start_level(const World& world, const std::string& level_filename, - const std::optional>& start_pos) + const std::optional>& start_pos, + bool skip_intro) { m_savegame = Savegame::from_current_profile(world.get_basename()); auto screen = std::make_unique(world.get_basedir(), level_filename, *m_savegame, - start_pos); + start_pos, + skip_intro); ScreenManager::current()->push_screen(std::move(screen)); if (!Editor::current()) m_savegame->get_profile().set_last_world(world.get_basename()); } -bool -GameManager::start_worldmap(const World& world, const std::string& worldmap_filename, - const std::string& sector, const std::string& spawnpoint) +void +GameManager::start_level(Level* level, + const std::optional>& start_pos, + bool skip_intro) { - try + m_levelstream.str(""); + m_levelstream.clear(); + Writer writer(m_levelstream); + level->save(writer); + auto screen = std::make_unique(m_levelstream); + if (start_pos) { - m_savegame = Savegame::from_current_profile(world.get_basename()); - - auto filename = m_savegame->get_player_status().last_worldmap; - // If we specified a worldmap filename manually, - // this overrides the default choice of "last worldmap". - if (!worldmap_filename.empty()) - { - filename = worldmap_filename; - } - - // No "last worldmap" found and no worldmap_filename - // specified. Let's go ahead and use the worldmap - // filename specified in the world. - if (filename.empty()) - { - filename = world.get_worldmap_filename(); - } - - auto worldmap = std::make_unique(filename, *m_savegame, sector, spawnpoint); - ScreenManager::current()->push_screen(std::move(worldmap)); - - if (!Editor::current()) - m_savegame->get_profile().set_last_world(world.get_basename()); + screen->set_start_pos(start_pos->first, start_pos->second); } - catch (const std::exception& e) + screen->restart_level(); + if (skip_intro) + screen->skip_intro(); + ScreenManager::current()->push_screen(std::move(screen)); +} + +worldmap::WorldMap* +GameManager::create_worldmap_instance(const World& world, const std::string& worldmap_filename, + const std::string& sector, const std::string& spawnpoint) +try +{ + m_savegame = Savegame::from_current_profile(world.get_basename()); + + auto filename = m_savegame->get_player_status().last_worldmap; + // If we specified a worldmap filename manually, + // this overrides the default choice of "last worldmap". + if (!worldmap_filename.empty()) { - log_warning << "Couldn't start worldmap: " << e.what() << std::endl; - return false; + filename = worldmap_filename; } + + // No "last worldmap" found and no worldmap_filename + // specified. Let's go ahead and use the worldmap + // filename specified in the world. + if (filename.empty()) + { + filename = world.get_worldmap_filename(); + } + + auto worldmap = new worldmap::WorldMap(filename, *m_savegame, sector, spawnpoint); + return worldmap; +} +catch (const std::exception& e) +{ + log_warning << "Couldn't start worldmap: " << e.what() << std::endl; + return nullptr; +} + +bool +GameManager::start_worldmap(const World& world, const std::string& worldmap_filename, + const std::string& sector, const std::string& spawnpoint) +{ + auto worldmap = std::unique_ptr( + create_worldmap_instance(world, worldmap_filename, sector, spawnpoint)); + if (!worldmap) + return false; + + ScreenManager::current()->push_screen(std::move(worldmap)); return true; } diff --git a/src/supertux/game_manager.hpp b/src/supertux/game_manager.hpp index 5cb4688dc3..fc3931a963 100644 --- a/src/supertux/game_manager.hpp +++ b/src/supertux/game_manager.hpp @@ -21,11 +21,16 @@ #include #include #include +#include #include "math/vector.hpp" class Savegame; class World; +class Level; +namespace worldmap { + class WorldMap; +} class GameManager final : public Currenton { @@ -34,15 +39,26 @@ class GameManager final : public Currenton void save(); + worldmap::WorldMap* create_worldmap_instance(const World& world, const std::string& worldmap_filename = "", + const std::string& sector = "", const std::string& spawnpoint = ""); bool start_worldmap(const World& world, const std::string& worldmap_filename = "", const std::string& sector = "", const std::string& spawnpoint = ""); bool start_worldmap(const World& world, const std::string& worldmap_filename, const std::optional>& start_pos); void start_level(const World& world, const std::string& level_filename, - const std::optional>& start_pos = std::nullopt); + const std::optional>& start_pos = std::nullopt, + bool skip_intro = false); + void start_level(Level* level, const std::optional>& start_pos = std::nullopt, + bool skip_intro = false); -private: +public: std::unique_ptr m_savegame; + +private: + std::unique_ptr m_current_level; + + // Must keep stringstream in memory or else GameSession can't restart. + std::stringstream m_levelstream; private: GameManager(const GameManager&) = delete; diff --git a/src/supertux/game_object.cpp b/src/supertux/game_object.cpp index c79f2c5959..667a5013ec 100644 --- a/src/supertux/game_object.cpp +++ b/src/supertux/game_object.cpp @@ -50,6 +50,31 @@ GameObject::GameObject(const ReaderMapping& reader) : reader.get("version", m_version, 1); } +GameObject::GameObject(GameObject* obj) : + m_parent(obj->m_parent), + m_name(obj->m_name), + m_type(obj->m_type), + m_fade_helpers(), + m_track_undo(obj->m_track_undo), + m_previous_type(obj->m_previous_type), + m_version(obj->m_version), + m_uid(obj->m_uid), + m_scheduled_for_removal(obj->m_scheduled_for_removal), + m_last_state(&*obj->m_last_state), + m_components(), + m_remove_listeners(obj->m_remove_listeners) +{ + for (auto &fade_helper : obj->m_fade_helpers) + { + m_fade_helpers.emplace_back(std::make_unique(fade_helper.get())); + } + + // for (auto &component : obj->m_components) + // { + // m_components.emplace_back(std::make_unique(component.get())); + // } +} + GameObject::~GameObject() { for (const auto& entry : m_remove_listeners) { @@ -105,10 +130,11 @@ GameObject::get_class_types() const ObjectSettings GameObject::get_settings() { - ObjectSettings result(get_display_name()); + ObjectSettings result(get_display_name(), get_uid()); result.add_int(_("Version"), &m_version, "version", 1, OPTION_HIDDEN); - result.add_text(_("Name"), &m_name, "name", ""); + result.add_text(_("Name"), &m_name, "name", "") + ->set_description(_("The name of this object. Must not contain any spaces if this object is to be referenced from scripts")); const GameObjectTypes types = get_types(); if (!types.empty()) @@ -122,7 +148,8 @@ GameObject::get_settings() names.push_back(type.name); } - result.add_enum(_("Type"), &m_type, names, ids, 0, "type"); + result.add_enum(_("Type"), &m_type, names, ids, 0, "type") + ->set_description(_("The type of an object defines its theme and behaviour. This is mostly used to distinguish between forest and ice world types of this object")); } return result; @@ -273,6 +300,7 @@ GameObject::register_class(ssq::VM& vm) { ssq::Class cls = vm.addAbstractClass("GameObject"); + cls.addFunc("get_uid", &GameObject::get_uid_value); cls.addFunc("get_version", &GameObject::get_version); cls.addFunc("get_latest_version", &GameObject::get_latest_version); cls.addFunc("is_up_to_date", &GameObject::is_up_to_date); diff --git a/src/supertux/game_object.hpp b/src/supertux/game_object.hpp index 820fd3ec7f..553de7070d 100644 --- a/src/supertux/game_object.hpp +++ b/src/supertux/game_object.hpp @@ -89,6 +89,7 @@ class GameObject : public ExposableClass public: GameObject(const std::string& name = ""); GameObject(const ReaderMapping& reader); + GameObject(GameObject* obj); virtual ~GameObject() override; /** Called after all objects have been added to the Sector and the @@ -265,6 +266,9 @@ class GameObject : public ExposableClass int type_id_to_value(const std::string& id) const; std::string type_value_to_id(int value) const; +private: + inline uint32_t get_uid_value() { return m_uid.get_value(); } + private: inline void set_uid(const UID& uid) { m_uid = uid; } diff --git a/src/supertux/game_object_manager.cpp b/src/supertux/game_object_manager.cpp index 6c1ff540c7..7922f90561 100644 --- a/src/supertux/game_object_manager.cpp +++ b/src/supertux/game_object_manager.cpp @@ -57,6 +57,33 @@ GameObjectManager::GameObjectManager(bool undo_tracking) : { } +GameObjectManager::GameObjectManager(GameObjectManager* gom) : + m_initialized(gom->m_initialized), + m_uid_generator(), + m_change_uid_generator(), + m_undo_tracking(gom->m_undo_tracking), + m_undo_stack_size(gom->m_undo_stack_size), + m_undo_stack(gom->m_undo_stack), + m_redo_stack(gom->m_redo_stack), + m_pending_change_stack(gom->m_pending_change_stack), + m_last_saved_change(gom->m_last_saved_change), + m_gameobjects(), + m_gameobjects_new(), + m_moved_object_uids(gom->m_moved_object_uids), + m_solid_tilemaps(gom->m_solid_tilemaps), + m_all_tilemaps(gom->m_all_tilemaps), + m_objects_by_name(), + m_objects_by_uid(), + m_objects_by_type_index(), + m_name_resolve_requests(gom->m_name_resolve_requests) +{ + for (auto &obj : gom->m_gameobjects) + { + m_gameobjects.emplace_back(obj.get()); + } + +} + GameObjectManager::~GameObjectManager() { // clear_objects() must be called before destructing the GameObjectManager. @@ -303,6 +330,7 @@ GameObjectManager::flush_game_objects() m_undo_stack.emplace_back(m_change_uid_generator.next(), std::move(m_pending_change_stack)); m_redo_stack.clear(); undo_stack_cleanup(); + update_editor_buttons(); } m_initialized = true; @@ -482,6 +510,8 @@ GameObjectManager::undo() m_redo_stack.push_back(std::move(change_set)); } m_undo_stack.pop_back(); + + update_editor_buttons(); } void @@ -511,6 +541,18 @@ GameObjectManager::redo() m_undo_stack.push_back(std::move(change_set)); } m_redo_stack.pop_back(); + + update_editor_buttons(); +} + +void +GameObjectManager::update_editor_buttons() +{ + if (Editor::current()) + { + Editor::current()->set_undo_disabled(m_undo_stack.empty()); + Editor::current()->set_redo_disabled(m_redo_stack.empty()); + } } void diff --git a/src/supertux/game_object_manager.hpp b/src/supertux/game_object_manager.hpp index 0bc5267916..391e6f3eca 100644 --- a/src/supertux/game_object_manager.hpp +++ b/src/supertux/game_object_manager.hpp @@ -59,6 +59,7 @@ class GameObjectManager : public ExposableClass public: GameObjectManager(bool undo_tracking = false); + GameObjectManager(GameObjectManager* gom); virtual ~GameObjectManager() override; virtual std::string get_exposed_class_name() const override { return "GameObjectManager"; } @@ -333,6 +334,8 @@ class GameObjectManager : public ExposableClass void this_before_object_add(GameObject& object); void this_before_object_remove(GameObject& object); + void update_editor_buttons(); + protected: /** An initial flush_game_objects() call has been initiated. */ bool m_initialized; diff --git a/src/supertux/game_session.cpp b/src/supertux/game_session.cpp index c6bc9e2ac1..30e0423cd7 100644 --- a/src/supertux/game_session.cpp +++ b/src/supertux/game_session.cpp @@ -57,18 +57,18 @@ static const int SHRINKFADE_LAYER = LAYER_LIGHTMAP - 1; static const float TELEPORT_FADE_TIME = 1.0f; -GameSession::GameSession(const std::string& levelfile_, Savegame& savegame, Statistics* statistics) : +GameSession::GameSession(Savegame* savegame, Statistics* statistics) : reset_button(false), reset_checkpoint_button(false), m_prevent_death(false), - m_level(), + m_level(nullptr), + m_level_storage(nullptr), m_statistics_backdrop(Surface::from_file("images/engine/menu/score-backdrop.png")), m_data_table(SquirrelVirtualMachine::current()->get_vm().findTable("Level").getOrCreateTable("data")), m_currentsector(nullptr), m_end_sequence(nullptr), m_game_pause(false), m_speed_before_pause(ScreenManager::current()->get_speed()), - m_levelfile(levelfile_), m_spawnpoints(), m_activated_checkpoint(), m_newsector(), @@ -78,6 +78,8 @@ GameSession::GameSession(const std::string& levelfile_, Savegame& savegame, Stat m_spawn_with_invincibility(false), m_best_level_statistics(statistics), m_savegame(savegame), + m_levelstream(nullptr), + m_tmp_playerstatus(0), m_play_time(0), m_levelintro_shown(false), m_coins_at_start(), @@ -97,6 +99,25 @@ GameSession::GameSession(const std::string& levelfile_, Savegame& savegame, Stat m_data_table.clear(); } + +GameSession::GameSession(Level* level, Savegame* savegame, Statistics* statistics) : + GameSession{savegame, statistics} +{ + m_level = level; +} + +GameSession::GameSession(const std::string& levelfile_, Savegame& savegame, Statistics* statistics) : + GameSession{&savegame, statistics} +{ + m_levelfile = levelfile_; +} + +GameSession::GameSession(std::istream& istream_, Savegame* savegame, Statistics* statistics) : + GameSession{savegame, statistics} +{ + m_levelstream = &istream_; +} + void GameSession::reset_level() { @@ -111,9 +132,12 @@ GameSession::reset_level() } } - PlayerStatus& currentStatus = m_savegame.get_player_status(); - currentStatus.coins = m_coins_at_start; - currentStatus.bonus = m_boni_at_start; + if (m_savegame) + { + PlayerStatus& currentStatus = m_savegame->get_player_status(); + currentStatus.coins = m_coins_at_start; + currentStatus.bonus = m_boni_at_start; + } clear_respawn_points(); m_activated_checkpoint = nullptr; @@ -127,12 +151,22 @@ GameSession::reset_level() void GameSession::on_player_added(int id) { - auto& player_status = m_savegame.get_player_status(); - if (player_status.m_num_players <= id) - player_status.add_player(); + PlayerStatus* player_status; + if (m_savegame) + { + player_status = &m_savegame->get_player_status(); + } + else + { + player_status = &m_tmp_playerstatus; + } + + if (player_status->m_num_players <= id) + player_status->add_player(); + // ID = 0 is impossible, so no need to write `(id == 0) ? "" : ...` - auto& player = m_currentsector->add(player_status, "Tux" + std::to_string(id + 1), id); + auto& player = m_currentsector->add(*player_status, "Tux" + std::to_string(id + 1), id); player.multiplayer_prepare_spawn(); } @@ -158,23 +192,26 @@ GameSession::on_player_removed(int id) void GameSession::restart_level(bool after_death, bool preserve_music) { - const PlayerStatus& currentStatus = m_savegame.get_player_status(); - m_coins_at_start = currentStatus.coins; - m_boni_at_start = currentStatus.bonus; - - // Needed for the title screen apparently. - if (m_currentsector) + if (m_savegame) { - try + const PlayerStatus& currentStatus = m_savegame->get_player_status(); + m_coins_at_start = currentStatus.coins; + m_boni_at_start = currentStatus.bonus; + + // Needed for the title screen apparently. + if (m_currentsector) { - for (const auto& p : m_currentsector->get_players()) + try + { + for (const auto& p : m_currentsector->get_players()) + { + p->set_bonus(m_boni_at_start.at(p->get_id()), false); + m_boni_at_start[p->get_id()] = currentStatus.bonus[p->get_id()]; + } + } + catch (const std::out_of_range&) { - p->set_bonus(m_boni_at_start.at(p->get_id()), false); - m_boni_at_start[p->get_id()] = currentStatus.bonus[p->get_id()]; } - } - catch (const std::out_of_range&) - { } } @@ -187,13 +224,25 @@ GameSession::restart_level(bool after_death, bool preserve_music) m_currentsector = nullptr; - const std::string base_dir = FileSystem::dirname(m_levelfile); - if (base_dir == "./") { - m_levelfile = FileSystem::basename(m_levelfile); - } - try { - m_level = LevelParser::from_file(m_levelfile, false, false); + if (FileSystem::dirname(m_levelfile) == "./") { + m_levelfile = FileSystem::basename(m_levelfile); + } + + // TODO: We currently storage the level as a stringstream, not ideal. + // Level was passed as an argument (likely from the editor) + // if (m_level == nullptr && !m_levelfile.empty()) + + if (!m_levelstream) + m_level_storage = LevelParser::from_file(m_levelfile, false, false); + else + { + m_levelstream->clear(); + m_levelstream->seekg(0, std::ios::beg); + m_level_storage = LevelParser::from_stream(*m_levelstream, "", false, false); + } + + m_level = m_level_storage.get(); /* Determine the spawnpoint to spawn/respawn Tux to. */ const GameSession::SpawnPoint* spawnpoint = nullptr; @@ -252,14 +301,16 @@ GameSession::restart_level(bool after_death, bool preserve_music) } catch (std::exception& e) { throw std::runtime_error(std::string("Couldn't start level: ") + e.what()); + // ScreenManager::current()->pop_screen(); + //return (-1); } - if (m_levelintro_shown) + if (m_levelintro_shown && !m_skip_intro) { const Vector shrinkpos = get_fade_point(); ScreenManager::current()->set_screen_fade(std::make_unique(shrinkpos, TELEPORT_FADE_TIME, SHRINKFADE_LAYER, ShrinkFade::FADEIN)); } - + if (!preserve_music) { auto& music_object = m_currentsector->get_singleton_by_type(); @@ -409,8 +460,11 @@ GameSession::abort_level() } } - PlayerStatus& currentStatus = m_savegame.get_player_status(); - currentStatus.coins = m_coins_at_start; + if (m_savegame) + { + PlayerStatus& currentStatus = m_savegame->get_player_status(); + currentStatus.coins = m_coins_at_start; + } SoundManager::current()->stop_sounds(); } @@ -423,6 +477,8 @@ GameSession::is_active() const void GameSession::check_end_conditions() { + if (!m_currentsector) + return; bool all_dead = true; for (const auto* p : m_currentsector->get_players()) if (!(all_dead &= p->is_dead())) @@ -483,10 +539,10 @@ GameSession::setup() m_currentsector->get_singleton_by_type().play_music(LEVEL_MUSIC); int total_stats_to_be_collected = m_level->m_stats.m_total_coins + m_level->m_stats.m_total_badguys + m_level->m_stats.m_total_secrets; - if ((!m_levelintro_shown) && (total_stats_to_be_collected > 0)) { + if ((!m_levelintro_shown) && (total_stats_to_be_collected > 0) && m_savegame && !m_skip_intro) { m_levelintro_shown = true; m_active = false; - ScreenManager::current()->push_screen(std::make_unique(*m_level, m_best_level_statistics, m_savegame.get_player_status())); + ScreenManager::current()->push_screen(std::make_unique(*m_level, m_best_level_statistics, m_savegame->get_player_status())); ScreenManager::current()->set_screen_fade(std::make_unique(FadeToBlack::FADEIN, TELEPORT_FADE_TIME)); } else @@ -494,6 +550,7 @@ GameSession::setup() const Vector shrinkpos = get_fade_point(); ScreenManager::current()->set_screen_fade(std::make_unique(shrinkpos, TELEPORT_FADE_TIME, SHRINKFADE_LAYER, ShrinkFade::FADEIN)); } + m_skip_intro = false; m_end_seq_started = false; } @@ -630,7 +687,8 @@ GameSession::update(float dt_sec, const Controller& controller) { if (player->is_active() && player->is_scripting_activated() && player->get_controller().pressed(Control::ITEM) && - m_savegame.get_player_status().m_item_pockets.size() > 0) + m_savegame && + m_savegame->get_player_status().m_item_pockets.size() > 0) { player->get_status().give_item_from_pocket(player); } @@ -725,7 +783,7 @@ GameSession::finish(bool win) if (win) { if (WorldMapSector::current()) { - WorldMapSector::current()->finished_level(m_level.get()); + WorldMapSector::current()->finished_level(m_level); } if (LevelsetScreen::current()) diff --git a/src/supertux/game_session.hpp b/src/supertux/game_session.hpp index 55e8ccac55..30f8667d5a 100644 --- a/src/supertux/game_session.hpp +++ b/src/supertux/game_session.hpp @@ -20,6 +20,7 @@ #include "util/currenton.hpp" #include +#include #include #include #include @@ -34,12 +35,12 @@ #include "supertux/screen_fade.hpp" #include "supertux/sequence.hpp" #include "supertux/timer.hpp" +#include "supertux/level.hpp" #include "video/surface_ptr.hpp" class CodeController; class DrawingContext; class EndSequence; -class Level; class Player; class Sector; class Statistics; @@ -77,7 +78,10 @@ class GameSession final : public Screen, }; public: + GameSession(Savegame* savegame = nullptr, Statistics* statistics = nullptr); + GameSession(Level* level, Savegame* savegame = nullptr, Statistics* statistics = nullptr); GameSession(const std::string& levelfile, Savegame& savegame, Statistics* statistics = nullptr); + GameSession(std::istream& istream, Savegame* savegame = nullptr, Statistics* statistics = nullptr); virtual void draw(Compositor& compositor) override; virtual void update(float dt_sec, const Controller& controller) override; @@ -134,8 +138,9 @@ class GameSession final : public Screen, void toggle_pause(); void abort_level(); bool is_active() const; + inline void skip_intro() { m_skip_intro = true; } - inline Savegame& get_savegame() const { return m_savegame; } + inline Savegame& get_savegame() const { return *m_savegame; } void set_scheduler(SquirrelScheduler& new_scheduler); @@ -156,7 +161,8 @@ class GameSession final : public Screen, bool m_prevent_death; /**< true if players should enter ghost mode instead of dying */ private: - std::unique_ptr m_level; + Level* m_level; + std::unique_ptr m_level_storage; SurfacePtr m_statistics_backdrop; ssq::Table m_data_table; @@ -177,18 +183,22 @@ class GameSession final : public Screen, // the sector and spawnpoint we should spawn after this frame std::string m_newsector; std::string m_newspawnpoint; + std::istream* m_levelstream; ScreenFade::FadeType m_spawn_fade_type; Timer m_spawn_fade_timer; bool m_spawn_with_invincibility; Statistics* m_best_level_statistics; - Savegame& m_savegame; + Savegame* m_savegame; + + PlayerStatus m_tmp_playerstatus; // Note: m_play_time should reset when a level is restarted from the beginning // but NOT if Tux respawns at a checkpoint (for LevelTimes to work) float m_play_time; /**< total time in seconds that this session ran interactively */ bool m_levelintro_shown; /**< true if the LevelIntro screen was already shown */ + bool m_skip_intro; /**< Manually skipped the intro from outside this class */ int m_coins_at_start; /** How many coins does the player have at the start */ std::vector m_boni_at_start; /** What boni does the player have at the start */ diff --git a/src/supertux/gameconfig.cpp b/src/supertux/gameconfig.cpp index 6d875fc68d..b6450a6ddf 100644 --- a/src/supertux/gameconfig.cpp +++ b/src/supertux/gameconfig.cpp @@ -103,6 +103,7 @@ Config::Config() : editor_render_grid(true), editor_snap_to_grid(true), editor_render_background(true), + editor_render_animations(true), editor_render_lighting(false), editor_autotile_mode(false), editor_autotile_help(true), @@ -110,6 +111,9 @@ Config::Config() : editor_undo_tracking(true), editor_undo_stack_size(20), editor_show_deprecated_tiles(false), + editor_show_properties_sidebar(true), + editor_show_toolbar_widgets(true), + editor_blur(15), multiplayer_auto_manage_players(true), multiplayer_multibind(false), #if SDL_VERSION_ATLEAST(2, 0, 9) @@ -120,7 +124,8 @@ Config::Config() : // and those with an older SDL; they won't have to check the setting each time. multiplayer_buzz_controllers(false), #endif - repository_url() + repository_url(), + preferred_text_editor() { } @@ -235,6 +240,7 @@ Config::load() editor_mapping->get("autotile_help", editor_autotile_help); editor_mapping->get("autotile_mode", editor_autotile_mode); editor_mapping->get("render_background", editor_render_background); + editor_mapping->get("render_animations", editor_render_animations); editor_mapping->get("render_grid", editor_render_grid); editor_mapping->get("render_lighting", editor_render_lighting); editor_mapping->get("selected_snap_grid_size", editor_selected_snap_grid_size); @@ -247,6 +253,9 @@ Config::load() editor_undo_stack_size = 1; } editor_mapping->get("show_deprecated_tiles", editor_show_deprecated_tiles); + editor_mapping->get("show_properties_sidebar", editor_show_properties_sidebar); + editor_mapping->get("show_toolbar_widgets", editor_show_toolbar_widgets); + editor_mapping->get("blur", editor_blur); } if (is_christmas()) { @@ -260,6 +269,7 @@ Config::load() config_mapping.get("multiplayer_auto_manage_players", multiplayer_auto_manage_players); config_mapping.get("multiplayer_multibind", multiplayer_multibind); config_mapping.get("multiplayer_buzz_controllers", multiplayer_buzz_controllers); + config_mapping.get("preferred_text_editor", preferred_text_editor); std::optional config_video_mapping; if (config_mapping.get("video", config_video_mapping)) @@ -410,6 +420,7 @@ Config::save() writer.write("multiplayer_auto_manage_players", multiplayer_auto_manage_players); writer.write("multiplayer_multibind", multiplayer_multibind); writer.write("multiplayer_buzz_controllers", multiplayer_buzz_controllers); + writer.write("preferred_text_editor", preferred_text_editor); writer.start_list("interface_colors"); writer.write("menubackcolor", menubackcolor.toVector()); @@ -497,6 +508,7 @@ Config::save() writer.write("autotile_help", editor_autotile_help); writer.write("autotile_mode", editor_autotile_mode); writer.write("render_background", editor_render_background); + writer.write("render_animations", editor_render_animations); writer.write("render_grid", editor_render_grid); writer.write("render_lighting", editor_render_lighting); writer.write("selected_snap_grid_size", editor_selected_snap_grid_size); @@ -504,6 +516,9 @@ Config::save() writer.write("undo_tracking", editor_undo_tracking); writer.write("undo_stack_size", editor_undo_stack_size); writer.write("show_deprecated_tiles", editor_show_deprecated_tiles); + writer.write("show_properties_sidebar", editor_show_properties_sidebar); + writer.write("show_toolbar_widgets", editor_show_toolbar_widgets); + writer.write("blur", editor_blur); } writer.end_list("editor"); diff --git a/src/supertux/gameconfig.hpp b/src/supertux/gameconfig.hpp index 2607b5dbaa..5e4bb1507e 100644 --- a/src/supertux/gameconfig.hpp +++ b/src/supertux/gameconfig.hpp @@ -144,6 +144,7 @@ class Config final bool editor_render_grid; bool editor_snap_to_grid; bool editor_render_background; + bool editor_render_animations; bool editor_render_lighting; bool editor_autotile_mode; bool editor_autotile_help; @@ -151,6 +152,10 @@ class Config final bool editor_undo_tracking; int editor_undo_stack_size; bool editor_show_deprecated_tiles; + bool editor_show_properties_sidebar; + bool editor_show_toolbar_widgets; + int editor_blur; + std::string preferred_text_editor; bool multiplayer_auto_manage_players; bool multiplayer_multibind; diff --git a/src/supertux/level.cpp b/src/supertux/level.cpp index de92a92bcd..ca39dfe8b4 100644 --- a/src/supertux/level.cpp +++ b/src/supertux/level.cpp @@ -54,7 +54,8 @@ Level::Level(bool worldmap) : m_skip_cutscene(false), m_icon(), m_icon_locked(), - m_wmselect_bkg() + m_wmselect_bkg(), + m_saving_in_progress(false) { s_current = this; @@ -64,6 +65,35 @@ Level::Level(bool worldmap) : } } +Level::Level(Level* level) : + m_is_worldmap(level->m_is_worldmap), + m_name(level->m_name), + m_author(level->m_author), + m_contact(level->m_contact), + m_license(level->m_license), + m_filename(level->m_filename), + m_note(level->m_note), + m_sectors(), + m_stats(), + m_target_time(level->m_target_time), + m_tileset(level->m_tileset), + m_allow_item_pocket(level->m_allow_item_pocket), + m_suppress_pause_menu(level->m_suppress_pause_menu), + m_is_in_cutscene(level->m_is_in_cutscene), + m_skip_cutscene(level->m_skip_cutscene), + m_icon(level->m_icon), + m_icon_locked(level->m_icon_locked), + m_wmselect_bkg(level->m_wmselect_bkg), + m_saving_in_progress(level->m_saving_in_progress) +{ + s_current = this; + + for (auto §ors : level->m_sectors) + { + m_sectors.push_back(std::make_unique(sectors.get())); + } +} + Level::~Level() { m_sectors.clear(); @@ -174,6 +204,8 @@ Level::save(const std::string& filepath, bool retry) void Level::save(Writer& writer) { + m_saving_in_progress = true; + writer.start_list("supertux-level"); // Starts writing to supertux level file. Keep this at the very beginning. @@ -220,6 +252,8 @@ Level::save(Writer& writer) // Ends writing to supertux level file. Keep this at the very end. writer.end_list("supertux-level"); + + m_saving_in_progress = false; } std::string diff --git a/src/supertux/level.hpp b/src/supertux/level.hpp index ea5476b4f3..368b7e4d5c 100644 --- a/src/supertux/level.hpp +++ b/src/supertux/level.hpp @@ -39,11 +39,13 @@ class Level final public: explicit Level(bool m_is_worldmap); + Level(Level* level); ~Level(); // saves to a levelfile void save(const std::string& filename, bool retry = false); void save(std::ostream& stream); + void save(Writer& writer); void add_sector(std::unique_ptr sector); inline const std::string& get_name() const { return m_name; } @@ -63,16 +65,17 @@ class Level final int get_total_badguys() const; int get_total_secrets() const; + bool is_saving_in_progress() const { return m_saving_in_progress; } + void reactivate(); inline bool is_worldmap() const { return m_is_worldmap; } inline const std::string& get_license() const { return m_license; } -private: void initialize(); - - void save(Writer& writer); + +private: void load_old_format(const ReaderMapping& reader); public: @@ -113,6 +116,9 @@ class Level final std::string m_icon_locked; std::string m_wmselect_bkg; +private: + bool m_saving_in_progress; + private: Level(const Level&) = delete; Level& operator=(const Level&) = delete; diff --git a/src/supertux/level_parser.cpp b/src/supertux/level_parser.cpp index de448c2287..b88b04e105 100644 --- a/src/supertux/level_parser.cpp +++ b/src/supertux/level_parser.cpp @@ -224,7 +224,7 @@ LevelParser::create(const std::string& filepath, const std::string& levelname) { m_level.m_filename = filepath; m_level.m_name = levelname; - m_level.m_license = "CC-BY-SA 4.0 International"; + m_level.m_license = LEVEL_DEFAULT_LICENSE; m_level.m_tileset = m_worldmap ? "images/ice_world.strf" : "images/tiles.strf"; auto sector = SectorParser::from_nothing(m_level); diff --git a/src/supertux/level_parser.hpp b/src/supertux/level_parser.hpp index e1c46776e0..88c320a556 100644 --- a/src/supertux/level_parser.hpp +++ b/src/supertux/level_parser.hpp @@ -23,6 +23,8 @@ class Level; class ReaderDocument; class ReaderMapping; +static std::string_view LEVEL_DEFAULT_LICENSE = "CC-BY-SA 4.0 International"; + class LevelParser final { public: diff --git a/src/supertux/levelset_screen.cpp b/src/supertux/levelset_screen.cpp index 36e05625e6..a95c9b5e7c 100644 --- a/src/supertux/levelset_screen.cpp +++ b/src/supertux/levelset_screen.cpp @@ -29,13 +29,15 @@ LevelsetScreen::LevelsetScreen(const std::string& basedir, const std::string& level_filename, Savegame& savegame, - const std::optional>& start_pos) : + const std::optional>& start_pos, + bool skip_intro) : m_basedir(basedir), m_level_filename(level_filename), m_savegame(savegame), m_level_started(false), m_solved(false), - m_start_pos(start_pos) + m_start_pos(start_pos), + m_skip_intro(skip_intro) { Levelset levelset(basedir); for (int i = 0; i < levelset.get_num_levels(); ++i) @@ -86,6 +88,11 @@ LevelsetScreen::setup() } else { auto screen = std::make_unique(FileSystem::join(m_basedir, m_level_filename), m_savegame); + if (m_skip_intro) + { + screen->skip_intro(); + m_skip_intro = false; + } if (m_start_pos) { screen->set_start_pos(m_start_pos->first, m_start_pos->second); } diff --git a/src/supertux/levelset_screen.hpp b/src/supertux/levelset_screen.hpp index b3c2a4e76f..31ade67c33 100644 --- a/src/supertux/levelset_screen.hpp +++ b/src/supertux/levelset_screen.hpp @@ -37,7 +37,7 @@ class LevelsetScreen final : public Screen, public: LevelsetScreen(const std::string& basedir, const std::string& level_filename, Savegame& savegame, - const std::optional>& start_pos); + const std::optional>& start_pos, bool skip_intro = false); virtual void draw(Compositor& compositor) override; virtual void update(float dt_sec, const Controller& controller) override; @@ -51,6 +51,7 @@ class LevelsetScreen final : public Screen, private: std::optional> m_start_pos; + bool m_skip_intro; LevelsetScreen(const LevelsetScreen&) = delete; LevelsetScreen& operator=(const LevelsetScreen&) = delete; diff --git a/src/supertux/main.cpp b/src/supertux/main.cpp index 1ac3c34220..4ca182515d 100644 --- a/src/supertux/main.cpp +++ b/src/supertux/main.cpp @@ -799,8 +799,9 @@ Main::release_check() const std::string version = version_full.substr(version_full.find("v") + 1, version_full.find("-") - 1); if (version != latest_ver) { - auto notif = std::make_unique("new_release_" + latest_ver); + auto notif = std::make_unique("new_release_" + latest_ver, 20.f, false, true); notif->set_text(fmt::format(fmt::runtime(_("New release: SuperTux v{}!")), latest_ver)); + notif->set_mini_text(_("Click for more details.")); notif->on_press([latest_ver]() { Dialog::show_confirmation(fmt::format(fmt::runtime(_("A new release of SuperTux (v{}) is available!\nFor more information, you can visit the SuperTux website.\n\nDo you want to visit the website now?")), latest_ver), []() diff --git a/src/supertux/menu/debug_menu.cpp b/src/supertux/menu/debug_menu.cpp index f6b07edbc1..55751da614 100644 --- a/src/supertux/menu/debug_menu.cpp +++ b/src/supertux/menu/debug_menu.cpp @@ -87,10 +87,6 @@ DebugMenu::DebugMenu() : DebugMenu::~DebugMenu() { - auto editor = Editor::current(); - - if (editor == nullptr) return; - editor->m_reactivate_request = true; } void diff --git a/src/supertux/menu/editor_level_select_menu.cpp b/src/supertux/menu/editor_level_select_menu.cpp index 2df5765658..75daed2707 100644 --- a/src/supertux/menu/editor_level_select_menu.cpp +++ b/src/supertux/menu/editor_level_select_menu.cpp @@ -57,8 +57,6 @@ EditorLevelSelectMenu::reload_menu() void EditorLevelSelectMenu::initialize() { - Editor::current()->m_deactivate_request = true; - World* world = get_world(); auto basedir = world->get_basedir(); m_levelset = std::unique_ptr(new Levelset(basedir, /* recursively = */ true)); @@ -100,11 +98,6 @@ EditorLevelSelectMenu::initialize() EditorLevelSelectMenu::~EditorLevelSelectMenu() { - auto editor = Editor::current(); - if (editor == nullptr) { - return; - } - editor->m_reactivate_request = true; } World* diff --git a/src/supertux/menu/editor_levelset_select_menu.cpp b/src/supertux/menu/editor_levelset_select_menu.cpp index b3a18a1f29..9463232fe4 100644 --- a/src/supertux/menu/editor_levelset_select_menu.cpp +++ b/src/supertux/menu/editor_levelset_select_menu.cpp @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +#include "supertux/constants.hpp" #include "supertux/menu/editor_level_select_menu.hpp" #include @@ -25,16 +26,20 @@ #include "gui/menu_manager.hpp" #include "physfs/util.hpp" #include "supertux/levelset.hpp" +#include "supertux/level.hpp" #include "supertux/menu/editor_levelset_select_menu.hpp" #include "supertux/menu/editor_delete_levelset_menu.hpp" +#include "supertux/menu/editor_temp_save_as.hpp" #include "supertux/menu/menu_storage.hpp" +#include "supertux/sector_parser.hpp" #include "supertux/world.hpp" #include "util/file_system.hpp" #include "util/gettext.hpp" #include "util/log.hpp" -EditorLevelsetSelectMenu::EditorLevelsetSelectMenu() : - m_contrib_worlds() +EditorLevelsetSelectMenu::EditorLevelsetSelectMenu(bool save_as) : + m_contrib_worlds(), + m_save_as(save_as) { initialize(); } @@ -47,14 +52,12 @@ EditorLevelsetSelectMenu::~EditorLevelsetSelectMenu() } if (!editor->is_level_loaded() && !editor->m_reload_request) { editor->m_quit_request = true; - } else { - editor->m_reactivate_request = true; } } + void EditorLevelsetSelectMenu::initialize() { - Editor::current()->m_deactivate_request = true; m_contrib_worlds.clear(); // Generating contrib levels list by making use of Level Subset @@ -69,7 +72,7 @@ EditorLevelsetSelectMenu::initialize() return false; }); - add_label(_("Choose World")); + add_label(m_save_as ? _("Save Level as") : _("Choose World")); add_hl(); int i = 0; @@ -125,9 +128,18 @@ EditorLevelsetSelectMenu::menu_action(MenuItem& item) { if (item.get_id() >= 0) { - std::unique_ptr menu = std::unique_ptr(new EditorLevelSelectMenu( - World::from_directory(m_contrib_worlds[item.get_id()]), this)); - MenuManager::instance().push_menu(std::move(menu)); + if (m_save_as) + { + std::unique_ptr menu = std::make_unique( + World::from_directory(m_contrib_worlds[item.get_id()])); + MenuManager::instance().push_menu(std::move(menu)); + } + else + { + std::unique_ptr menu = std::unique_ptr(new EditorLevelSelectMenu( + World::from_directory(m_contrib_worlds[item.get_id()]), this)); + MenuManager::instance().push_menu(std::move(menu)); + } } else if (item.get_id() == -3) { diff --git a/src/supertux/menu/editor_levelset_select_menu.hpp b/src/supertux/menu/editor_levelset_select_menu.hpp index 1215a06091..b3f4447ef4 100644 --- a/src/supertux/menu/editor_levelset_select_menu.hpp +++ b/src/supertux/menu/editor_levelset_select_menu.hpp @@ -18,13 +18,14 @@ #include "gui/menu.hpp" -class EditorLevelsetSelectMenu final : public Menu +class EditorLevelsetSelectMenu : public Menu { private: std::vector m_contrib_worlds; + const bool m_save_as; public: - EditorLevelsetSelectMenu(); + EditorLevelsetSelectMenu(bool save_as = false); ~EditorLevelsetSelectMenu() override; void menu_action(MenuItem& item) override; diff --git a/src/supertux/menu/editor_menu.cpp b/src/supertux/menu/editor_menu.cpp index c31a04a2ec..e988babbf3 100644 --- a/src/supertux/menu/editor_menu.cpp +++ b/src/supertux/menu/editor_menu.cpp @@ -49,11 +49,7 @@ EditorMenu::refresh() bool worldmap = Editor::current()->get_level()->is_worldmap(); bool is_world = Editor::current()->get_world() != nullptr; - std::vector snap_grid_sizes; - snap_grid_sizes.push_back(_("tiny tile (4px)")); - snap_grid_sizes.push_back(_("small tile (8px)")); - snap_grid_sizes.push_back(_("medium tile (16px)")); - snap_grid_sizes.push_back(_("big tile (32px)")); + bool is_temp_level = Editor::current()->is_temp_level(); add_label(_("Level Editor")); add_hl(); @@ -61,7 +57,8 @@ EditorMenu::refresh() add_entry(MNID_SAVELEVEL, worldmap ? _("Save Worldmap") : _("Save Level")); if (!worldmap) { add_entry(MNID_SAVEASLEVEL, _("Save Level as")); - add_entry(MNID_SAVECOPYLEVEL, _("Save Copy")); + if (!is_temp_level) + add_entry(MNID_SAVECOPYLEVEL, _("Save Copy")); add_entry(MNID_TESTLEVEL, _("Test Level")); } else { add_entry(MNID_TESTLEVEL, _("Test Worldmap")); @@ -69,37 +66,24 @@ EditorMenu::refresh() add_entry(MNID_OPTIONS, _("Options")); - add_entry(MNID_SHARE, _("Share Level")); - - add_entry(MNID_PACK, _("Package Add-On")); - - add_entry(MNID_OPEN_DIR, _("Open Level Directory")); + if (!is_temp_level) + { + add_entry(MNID_PACK, _("Package Add-On")); + add_entry(MNID_OPEN_DIR, _("Open Level Directory")); + } - if (is_world) + if (is_world && !is_temp_level) add_entry(MNID_LEVELSEL, _("Edit Another Level")); add_entry(MNID_LEVELSETSEL, _("Edit Another World")); - add_hl(); - - add_submenu(_("Convert Tiles"), MenuStorage::EDITOR_CONVERTERS_MENU) - .set_help(_("Convert all tiles in the level using converters.")); - - add_hl(); - - add_string_select(-1, _("Grid Size"), &(g_config->editor_selected_snap_grid_size), snap_grid_sizes); - add_toggle(-1, _("Show Grid"), &(g_config->editor_render_grid)); - add_toggle(-1, _("Grid Snapping"), &(g_config->editor_snap_to_grid)); - add_toggle(-1, _("Render Background"), &(g_config->editor_render_background)); - add_toggle(-1, _("Render Light"), &(Compositor::s_render_lighting)); - add_toggle(-1, _("Autotile Mode"), &(g_config->editor_autotile_mode)); - add_toggle(-1, _("Enable Autotile Help"), &(g_config->editor_autotile_help)); - add_toggle(-1, _("Enable Object Undo Tracking"), &(g_config->editor_undo_tracking)); - if (g_config->editor_undo_tracking) + if (!is_temp_level) { - add_intfield(_("Undo Stack Size"), &(g_config->editor_undo_stack_size), -1, true); + add_hl(); + + add_submenu(_("Convert Tiles"), MenuStorage::EDITOR_CONVERTERS_MENU) + .set_help(_("Convert all tiles in the level using converters.")); } - add_intfield(_("Autosave Frequency"), &(g_config->editor_autosave_frequency)); if (Editor::current()->has_deprecated_tiles()) { @@ -112,6 +96,8 @@ EditorMenu::refresh() } add_hl(); + + add_submenu(_("Editor settings"), MenuStorage::EDITOR_SETTINGS_MENU); add_submenu(worldmap ? _("Worldmap Settings") : _("Level Settings"), MenuStorage::EDITOR_LEVEL_MENU); @@ -123,12 +109,6 @@ EditorMenu::refresh() EditorMenu::~EditorMenu() { - auto editor = Editor::current(); - - if (editor == nullptr) - return; - - editor->m_reactivate_request = true; } void @@ -191,52 +171,45 @@ EditorMenu::menu_action(MenuItem& item) MenuManager::instance().push_menu(MenuStorage::OPTIONS_MENU); break; - case MNID_SHARE: - { - Dialog::show_confirmation(_("We encourage you to share your levels in the SuperTux forum.\nTo find your level, click the\n\"Open Level directory\" menu item.\nDo you want to go to the forum now?"), [] { - FileSystem::open_url("https://groups.f-hub.org/supertux"); - }); - } - break; - - case MNID_HELP: + case MNID_HELP: { auto dialog = std::make_unique(); auto help_dialog_text = - _("Keyboard Shortcuts:") + "\n" + - "---------------------" + "\n" + - _("Esc = Open Menu") + "\n" + - _("Ctrl+S = Save") + "\n" + - _("Ctrl+T = Test") + "\n" + - _("Ctrl+Z = Undo") + "\n" + - _("Ctrl+Y = Redo") + "\n" + - _("F5 = Toggle Autotiling") + "\n" + - _("F6 = Render Light") + "\n" + - _("F7 = Grid Snapping") + "\n" + - _("F8 = Show Grid") + "\n" + - _("Ctrl++ or Ctrl+Scroll Up = Zoom In") + "\n" + - _("Ctrl+- or Ctrl+Scroll Down = Zoom Out") + "\n" + - _("Ctrl+D = Reset Zoom") + "\n\n" + - _("Scripting Shortcuts:") + "\n" + - "-------------" + "\n" + + _("Keyboard Shortcuts") + ":\n" + + "Esc = " + _("Open Menu") + "\n" + + "Ctrl+S = " + _("Save") + "\n" + + "Ctrl+T = " + _("Test") + "\n" + + "Ctrl+Shift+T = " + _("Test at Cursor") + "\n" + + "Ctrl+Alt+Shift+T = " + _("Test at Last Position") + "\n" + + "Ctrl+Z = " + _("Undo") + "\n" + + "Ctrl+Y = " + _("Redo") + "\n" + + "F5 = " + _("Toggle Autotiling") + "\n" + + "F6 = " + _("Render Light") + "\n" + + "F7 = " + _("Grid Snapping") + "\n" + + "F8 = " + _("Show Grid") + "\n" + + "Ctrl+X = " + _("Toggle Between Tileset/Objects Tool") + "\n" + + _("Ctrl+PLUS or Ctrl+Scroll Up = Zoom In") + "\n" + + _("Ctrl+MINUS or Ctrl+Scroll Down = Zoom Out") + "\n" + + "Ctrl+D = " + _("Reset Zoom") + "\n\n" + + _("Scripting Shortcuts") + ":\n" + _("Home = Go to beginning of line") + "\n" + _("End = Go to end of line") + "\n" + _("Left arrow = Go back in text") + "\n" + _("Right arrow = Go forward in text") + "\n" + _("Backspace = Delete in front of text cursor") + "\n" + _("Delete = Delete behind text cursor") + "\n" + - _("Ctrl+X = Cut whole line") + "\n" + - _("Ctrl+C = Copy whole line") + "\n" + - _("Ctrl+V = Paste") + "\n" + - _("Ctrl+D = Duplicate line") + "\n" + - _("Ctrl+Z = Undo") + "\n" + - _("Ctrl+Y = Redo"); + "Ctrl+X = " + _("Cut whole line") + "\n" + + "Ctrl+C = " + _("Copy whole line") + "\n" + + "Ctrl+V = " + _("Paste") + "\n" + + "Ctrl+D = " + _("Duplicate line") + "\n" + + "Ctrl+Z = " + _("Undo") + "\n" + + "Ctrl+Y = " + _("Redo"); dialog->set_text(help_dialog_text); dialog->add_cancel_button(_("Got it!")); MenuManager::instance().set_dialog(std::move(dialog)); } - break; + break; case MNID_LEVELSEL: editor->check_unsaved_changes([] { diff --git a/src/supertux/menu/editor_menu.hpp b/src/supertux/menu/editor_menu.hpp index e9ed565662..795d33845f 100644 --- a/src/supertux/menu/editor_menu.hpp +++ b/src/supertux/menu/editor_menu.hpp @@ -30,7 +30,6 @@ class EditorMenu final : public Menu MNID_OPTIONS, MNID_PACK, MNID_OPEN_DIR, - MNID_SHARE, MNID_LEVELSEL, MNID_LEVELSETSEL, MNID_HELP, diff --git a/src/supertux/menu/editor_objectgroup_menu.cpp b/src/supertux/menu/editor_objectgroup_menu.cpp index a738523da8..8dd440aa6c 100644 --- a/src/supertux/menu/editor_objectgroup_menu.cpp +++ b/src/supertux/menu/editor_objectgroup_menu.cpp @@ -40,15 +40,11 @@ EditorObjectgroupMenu::EditorObjectgroupMenu() add_hl(); add_entry(-1,_("Cancel")); + allow_click_when_unfocused(); } EditorObjectgroupMenu::~EditorObjectgroupMenu() { - auto editor = Editor::current(); - if (editor == nullptr) { - return; - } - editor->m_reactivate_request = true; } void diff --git a/src/supertux/menu/editor_save_as.cpp b/src/supertux/menu/editor_save_as.cpp index c53a6a6c47..1894135a80 100644 --- a/src/supertux/menu/editor_save_as.cpp +++ b/src/supertux/menu/editor_save_as.cpp @@ -44,11 +44,6 @@ EditorSaveAs::EditorSaveAs(bool do_switch_file) : EditorSaveAs::~EditorSaveAs() { - auto editor = Editor::current(); - if (editor == nullptr) { - return; - } - editor->m_reactivate_request = true; } void diff --git a/src/supertux/menu/editor_sector_menu.cpp b/src/supertux/menu/editor_sector_menu.cpp index 8cfa0f8cad..ba6d9df8f1 100644 --- a/src/supertux/menu/editor_sector_menu.cpp +++ b/src/supertux/menu/editor_sector_menu.cpp @@ -34,7 +34,7 @@ EditorSectorMenu::EditorSectorMenu() : add_label(fmt::format(fmt::runtime(_("Sector {}")), sector->get_name())); add_hl(); add_textfield(_("Name"), §or->m_name); - add_script(_("Initialization script"), §or->m_init_script); + add_script(UID(), sector->m_name, _("Initialization script"), §or->m_init_script); add_toggle(0, _("Run initialization script only once"), §or->m_init_script_run_once); add_floatfield(_("Gravity"), §or->m_gravity); diff --git a/src/supertux/menu/editor_sectors_menu.cpp b/src/supertux/menu/editor_sectors_menu.cpp index 46c0b65c6c..22e33ee871 100644 --- a/src/supertux/menu/editor_sectors_menu.cpp +++ b/src/supertux/menu/editor_sectors_menu.cpp @@ -47,11 +47,6 @@ EditorSectorsMenu::EditorSectorsMenu() EditorSectorsMenu::~EditorSectorsMenu() { - auto editor = Editor::current(); - if (editor == nullptr) { - return; - } - editor->m_reactivate_request = true; } void @@ -78,7 +73,6 @@ EditorSectorsMenu::create_sector() level->add_sector(std::move(new_sector)); Editor::current()->load_sector(sector_name); MenuManager::instance().clear_menu_stack(); - Editor::current()->m_reactivate_request = true; } void diff --git a/src/supertux/menu/editor_settings.cpp b/src/supertux/menu/editor_settings.cpp new file mode 100644 index 0000000000..f0f5e90f97 --- /dev/null +++ b/src/supertux/menu/editor_settings.cpp @@ -0,0 +1,65 @@ +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "supertux/menu/editor_settings.hpp" + +#include "util/gettext.hpp" +#include "supertux/gameconfig.hpp" +#include "supertux/globals.hpp" +#include "video/compositor.hpp" + +EditorSettings::EditorSettings() +{ + add_label(_("Editor Settings")); + add_hl(); + + std::vector snap_grid_sizes; + snap_grid_sizes.push_back(_("tiny tile (4px)")); + snap_grid_sizes.push_back(_("small tile (8px)")); + snap_grid_sizes.push_back(_("medium tile (16px)")); + snap_grid_sizes.push_back(_("big tile (32px)")); + + add_string_select(-1, _("Grid Size"), &(g_config->editor_selected_snap_grid_size), snap_grid_sizes); + add_toggle(-1, _("Show Grid"), &(g_config->editor_render_grid)); + add_toggle(-1, _("Grid Snapping"), &(g_config->editor_snap_to_grid)); + add_toggle(-1, _("Render Background"), &(g_config->editor_render_background)); + add_toggle(-1, _("Render Light"), &(Compositor::s_render_lighting)); + add_toggle(-1, _("Render Animations"), &(g_config->editor_render_animations)); + add_toggle(-1, _("Autotile Mode"), &(g_config->editor_autotile_mode)); + add_toggle(-1, _("Enable Autotile Help"), &(g_config->editor_autotile_help)); + add_toggle(-1, _("Enable Object Undo Tracking"), &(g_config->editor_undo_tracking)); + add_toggle(-1, _("Show Properties Sidebar"), &(g_config->editor_show_properties_sidebar)); + add_toggle(-1, _("Show Toolbar"), &(g_config->editor_show_toolbar_widgets)); + add_intfield(_("Blur Amount"), &(g_config->editor_blur), -1, true); + add_textfield(_("Preferred Text Editor"), &(g_config->preferred_text_editor)); + if (g_config->editor_undo_tracking) + { + add_intfield(_("Undo Stack Size"), &(g_config->editor_undo_stack_size), -1, true); + } + add_intfield(_("Autosave Frequency"), &(g_config->editor_autosave_frequency)); + + add_hl(); + add_back(_("Back")); +} + +EditorSettings::~EditorSettings() +{ +} + +void +EditorSettings::menu_action(MenuItem& item) +{ +} diff --git a/src/supertux/menu/editor_settings.hpp b/src/supertux/menu/editor_settings.hpp new file mode 100644 index 0000000000..b2326714ff --- /dev/null +++ b/src/supertux/menu/editor_settings.hpp @@ -0,0 +1,39 @@ +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include "gui/menu.hpp" + +class World; + +class EditorSettings final : public Menu +{ +private: + enum MenuIDs { + MNID_CANCEL + }; + +public: + EditorSettings(); + ~EditorSettings() override; + + void menu_action(MenuItem& item) override; + +private: + EditorSettings(const EditorSettings&) = delete; + EditorSettings& operator=(const EditorSettings&) = delete; +}; diff --git a/src/supertux/menu/editor_temp_save_as.cpp b/src/supertux/menu/editor_temp_save_as.cpp new file mode 100644 index 0000000000..f8d2ad453b --- /dev/null +++ b/src/supertux/menu/editor_temp_save_as.cpp @@ -0,0 +1,100 @@ +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "supertux/menu/editor_temp_save_as.hpp" + +#include "editor/editor.hpp" +#include "gui/dialog.hpp" +#include "gui/menu_item.hpp" +#include "gui/menu_manager.hpp" +#include "gui/notification.hpp" +#include "supertux/level.hpp" +#include "supertux/gameconfig.hpp" +#include "supertux/menu/menu_storage.hpp" +#include "supertux/world.hpp" +#include "util/gettext.hpp" +#include "video/compositor.hpp" + +EditorTempSaveAs::EditorTempSaveAs(std::unique_ptr world) : + m_world(std::move(world)), + m_file_name() +{ + Level* level = Editor::current()->get_level(); + add_label(_("Save Level as")); + + add_hl(); + + add_textfield(_("Level name"), &(level->m_name)); + add_textfield(_("Author"), &(level->m_author)); + add_textfield(_("License"), &(level->m_license)); + + add_hl(); + add_entry(MNID_SAVE, _("Save")); + add_back(_("Cancel")); + + std::string dir; + int num = 0; + do { + num++; + m_file_name = "level" + std::to_string(num) + ".stl"; + dir = m_world->get_basedir() + "/" + m_file_name; + } while ( PHYSFS_exists(dir.c_str()) ); +} + +EditorTempSaveAs::~EditorTempSaveAs() +{ + auto editor = Editor::current(); + if (editor == nullptr) { + return; + } + editor->m_reactivate_request = true; +} + +void +EditorTempSaveAs::menu_action(MenuItem& item) +{ + auto editor = Editor::current(); + + switch (item.get_id()) + { + case MNID_SAVE: + { + Level* level = editor->get_level(); + + if (level->m_name.empty()) + { + Dialog::show_message(_("Please enter a name for this level.")); + return; + } + + editor->m_save_request = true; + editor->m_save_request_filename = m_file_name; + editor->m_save_temp_level = true; + + editor->set_world(std::move(std::unique_ptr(m_world.release()))); + + auto notif = std::make_unique("create_level_notif", 5.f, false, true); + notif->set_text(_("Level created!")); + MenuManager::instance().set_notification(std::move(notif)); + + MenuManager::instance().clear_menu_stack(); + } + break; + + default: + break; + } +} diff --git a/src/supertux/menu/editor_temp_save_as.hpp b/src/supertux/menu/editor_temp_save_as.hpp new file mode 100644 index 0000000000..6f4b61ef97 --- /dev/null +++ b/src/supertux/menu/editor_temp_save_as.hpp @@ -0,0 +1,43 @@ +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include "gui/menu.hpp" + +class World; + +class EditorTempSaveAs final : public Menu +{ +private: + enum MenuIDs { + MNID_SAVE + }; + +public: + EditorTempSaveAs(std::unique_ptr world); + ~EditorTempSaveAs() override; + + void menu_action(MenuItem& item) override; + +private: + std::unique_ptr m_world; + std::string m_file_name; + +private: + EditorTempSaveAs(const EditorTempSaveAs&) = delete; + EditorTempSaveAs& operator=(const EditorTempSaveAs&) = delete; +}; diff --git a/src/supertux/menu/editor_temp_save_menu.cpp b/src/supertux/menu/editor_temp_save_menu.cpp new file mode 100644 index 0000000000..0eb40bb60f --- /dev/null +++ b/src/supertux/menu/editor_temp_save_menu.cpp @@ -0,0 +1,60 @@ +// SuperTux +// Copyright (C) 2015 Hume2 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "supertux/menu/editor_temp_save_menu.hpp" + +#include + +#include "editor/editor.hpp" +#include "gui/dialog.hpp" +#include "gui/item_action.hpp" +#include "gui/item_goto.hpp" +#include "gui/item_toggle.hpp" +#include "gui/menu_manager.hpp" +#include "supertux/level.hpp" +#include "supertux/gameconfig.hpp" +#include "supertux/globals.hpp" +#include "supertux/menu/editor_levelset_select_menu.hpp" +#include "supertux/menu/editor_save_as.hpp" +#include "supertux/menu/menu_storage.hpp" +#include "util/gettext.hpp" +#include "video/compositor.hpp" + +#ifdef __EMSCRIPTEN__ +#include +#include +#endif + +EditorTempSaveMenu::EditorTempSaveMenu() : + EditorLevelsetSelectMenu(true) +{ +} + +EditorTempSaveMenu::~EditorTempSaveMenu() +{ +} + +void +EditorTempSaveMenu::initialize() +{ + EditorLevelsetSelectMenu::initialize(); +} + +void +EditorTempSaveMenu::menu_action(MenuItem& item) +{ + EditorLevelsetSelectMenu::menu_action(item); +} diff --git a/src/supertux/menu/editor_temp_save_menu.hpp b/src/supertux/menu/editor_temp_save_menu.hpp new file mode 100644 index 0000000000..e233f4e8a3 --- /dev/null +++ b/src/supertux/menu/editor_temp_save_menu.hpp @@ -0,0 +1,34 @@ +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include "gui/menu.hpp" +#include "supertux/menu/editor_levelset_select_menu.hpp" + +class EditorTempSaveMenu final : public EditorLevelsetSelectMenu +{ +public: + EditorTempSaveMenu(); + ~EditorTempSaveMenu() override; + + void menu_action(MenuItem& item) override; + void initialize(); + +private: + EditorTempSaveMenu(const EditorTempSaveMenu&) = delete; + EditorTempSaveMenu& operator=(const EditorTempSaveMenu&) = delete; +}; diff --git a/src/supertux/menu/editor_tilegroup_menu.cpp b/src/supertux/menu/editor_tilegroup_menu.cpp index a6429c2c71..1616b243ef 100644 --- a/src/supertux/menu/editor_tilegroup_menu.cpp +++ b/src/supertux/menu/editor_tilegroup_menu.cpp @@ -34,15 +34,11 @@ EditorTilegroupMenu::EditorTilegroupMenu() add_hl(); add_entry(-1,_("Cancel")); + allow_click_when_unfocused(); } EditorTilegroupMenu::~EditorTilegroupMenu() { - auto editor = Editor::current(); - if (editor == nullptr) { - return; - } - editor->m_reactivate_request = true; } void diff --git a/src/supertux/menu/game_menu.cpp b/src/supertux/menu/game_menu.cpp index 2f1668c2e6..bafb8748e7 100644 --- a/src/supertux/menu/game_menu.cpp +++ b/src/supertux/menu/game_menu.cpp @@ -16,17 +16,24 @@ #include "supertux/menu/game_menu.hpp" +#include "audio/sound_manager.hpp" +#include "editor/editor.hpp" #include "gui/dialog.hpp" #include "gui/menu_item.hpp" #include "gui/menu_manager.hpp" +#include "supertux/fadetoblack.hpp" +#include "supertux/game_manager.hpp" #include "supertux/game_session.hpp" #include "supertux/gameconfig.hpp" #include "supertux/globals.hpp" #include "supertux/level.hpp" #include "supertux/menu/menu_storage.hpp" +#include "supertux/screen_manager.hpp" #include "supertux/sector.hpp" #include "object/player.hpp" +#include "util/file_system.hpp" #include "util/gettext.hpp" +#include "worldmap/worldmap.hpp" GameMenu::GameMenu() : reset_callback ( [] { @@ -46,14 +53,23 @@ GameMenu::GameMenu() : { Level& level = GameSession::current()->get_current_level(); - add_label(level.m_name); - add_hl(); + if (!level.get_name().empty()) + { + add_label(level.get_name()); + add_hl(); + } + add_entry(MNID_CONTINUE, _("Continue")); add_entry(MNID_RESETLEVEL, _("Restart Level")); if (Sector::current()->get_players()[0]->get_status().can_reach_checkpoint()) { add_entry(MNID_RESETLEVELCHECKPOINT, _("Restart from Checkpoint")); } + + if (g_config->developer_mode && !Editor::current()) + { + add_entry(MNID_EDITLEVEL, _("Edit Level")); + } add_submenu(_("Options"), MenuStorage::INGAME_OPTIONS_MENU); add_hl(); @@ -92,6 +108,39 @@ GameMenu::menu_action(MenuItem& item) reset_checkpoint_callback(); } break; + + case MNID_EDITLEVEL: + { + if (Editor::is_active()) + break; + MenuManager::instance().clear_menu_stack(); + //Editor* editor = new Editor(); + //editor->disable_testing(); + std::string level_file = GameSession::current()->get_level_file(); + std::string return_to = worldmap::WorldMap::current()->get_levels_path(); + ScreenManager::current()->pop_screen(); + ScreenManager::current()->pop_screen(); + // We must queue the creation of the level queue or else the currenton gets clobbered + ScreenManager::current()->push_screen([level_file, return_to]() { + Editor* editor = new Editor(); + editor->set_level(level_file); + editor->update(0, Controller()); + editor->on_exit([return_to]() { + // Same as last comment... This restarts the previous level + ScreenManager::current()->push_screen([return_to]() { + // TODO: Move this somewhere else, it is similar to the GameManager::start_worldmap code + // Also, what if the world gets deleted in the middle of editing? + std::unique_ptr world = World::from_directory(FileSystem::strip_leading_dirs(return_to)); + auto worldmap = GameManager::current()->create_worldmap_instance(*world); + worldmap->start_level(); + return worldmap; + }); + }); + + return editor; + }); + } + break; case MNID_ABORTLEVEL: if (g_config->confirmation_dialog) diff --git a/src/supertux/menu/game_menu.hpp b/src/supertux/menu/game_menu.hpp index 00c125ed76..aa5d66a5ff 100644 --- a/src/supertux/menu/game_menu.hpp +++ b/src/supertux/menu/game_menu.hpp @@ -40,6 +40,7 @@ class GameMenu final : public Menu MNID_CONTINUE, MNID_RESETLEVEL, MNID_RESETLEVELCHECKPOINT, + MNID_EDITLEVEL, MNID_ABORTLEVEL }; diff --git a/src/supertux/menu/menu_storage.cpp b/src/supertux/menu/menu_storage.cpp index 7c3e189407..c417865cda 100644 --- a/src/supertux/menu/menu_storage.cpp +++ b/src/supertux/menu/menu_storage.cpp @@ -31,6 +31,8 @@ #include "supertux/menu/editor_levelset_select_menu.hpp" #include "supertux/menu/editor_new_levelset_menu.hpp" #include "supertux/menu/editor_objectgroup_menu.hpp" +#include "supertux/menu/editor_settings.hpp" +#include "supertux/menu/editor_temp_save_menu.hpp" #include "supertux/menu/editor_tilegroup_menu.hpp" #include "supertux/menu/editor_sector_menu.hpp" #include "supertux/menu/editor_sectors_menu.hpp" @@ -150,6 +152,12 @@ MenuStorage::create(MenuId menu_id) case EDITOR_MENU: return std::make_unique(); + + case EDITOR_SETTINGS_MENU: + return std::make_unique(); + + case EDITOR_TEMP_SAVE_MENU: + return std::make_unique(); case EDITOR_TILEGROUP_MENU: return std::make_unique(); diff --git a/src/supertux/menu/menu_storage.hpp b/src/supertux/menu/menu_storage.hpp index 31a15945c9..257185cb34 100644 --- a/src/supertux/menu/menu_storage.hpp +++ b/src/supertux/menu/menu_storage.hpp @@ -54,6 +54,8 @@ class MenuStorage final EDITOR_NEW_LEVELSET_MENU, EDITOR_LEVEL_SELECT_MENU, EDITOR_MENU, + EDITOR_SETTINGS_MENU, + EDITOR_TEMP_SAVE_MENU, EDITOR_TILEGROUP_MENU, EDITOR_OBJECTGROUP_MENU, EDITOR_SECTORS_MENU, diff --git a/src/supertux/moving_object.cpp b/src/supertux/moving_object.cpp index 9e1d38d253..052944f9b5 100644 --- a/src/supertux/moving_object.cpp +++ b/src/supertux/moving_object.cpp @@ -64,11 +64,13 @@ MovingObject::get_settings() if (has_variable_size()) { - result.add_float(_("Width"), &m_col.m_bbox.get_width(), "width", {}, OPTION_HIDDEN); - result.add_float(_("Height"), &m_col.m_bbox.get_height(), "height", {}, OPTION_HIDDEN); + result.add_float(_("Width"), &m_col.m_bbox.get_width(), "width", {}, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); + result.add_float(_("Height"), &m_col.m_bbox.get_height(), "height", {}, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); } - result.add_float(_("X"), &m_col.m_bbox.get_left(), "x", {}, OPTION_HIDDEN); - result.add_float(_("Y"), &m_col.m_bbox.get_top(), "y", {}, OPTION_HIDDEN); + result.add_float(_("X"), &m_col.m_bbox.get_left(), "x", {}, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES) + ->set_description(_("The horizontal location of the object in pixels from the left.")); + result.add_float(_("Y"), &m_col.m_bbox.get_top(), "y", {}, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES) + ->set_description(_("The vertical location of the object in pixels from the top.")); return result; } diff --git a/src/supertux/screen_manager.cpp b/src/supertux/screen_manager.cpp index 425babe382..a44795368a 100644 --- a/src/supertux/screen_manager.cpp +++ b/src/supertux/screen_manager.cpp @@ -159,7 +159,16 @@ ScreenManager::push_screen(std::unique_ptr screen, std::unique_ptr callback, std::unique_ptr screen_fade) +{ + log_debug << "ScreenManager::push_screen(): (lambda) " << &callback << std::endl; + assert(callback); + set_screen_fade(std::move(screen_fade)); + m_actions.emplace_back(Action::PUSH_ACTION, nullptr, std::move(callback)); } void @@ -516,8 +525,14 @@ ScreenManager::handle_screen_switch() break; case Action::PUSH_ACTION: - assert(action.screen); - m_screen_stack.push_back(std::move(action.screen)); + if (!action.screen && action.callback) + { + m_screen_stack.push_back(std::unique_ptr(action.callback())); + } + else + { + m_screen_stack.push_back(std::move(action.screen)); + } break; case Action::QUIT_ACTION: @@ -623,8 +638,8 @@ void ScreenManager::loop_iter() // limit the draw time offset to at most one step. float time_offset = m_speed * speed_multiplier * std::min(elapsed_time, seconds_per_step); - if ((steps > 0 && !m_screen_stack.empty()) - || always_draw) { + if (((steps > 0 && !m_screen_stack.empty()) + || always_draw) && m_actions.empty() || m_screen_fade) { // Draw a frame Compositor compositor(m_video_system, g_config->frame_prediction ? time_offset : 0.0f); draw(compositor, *m_fps_statistics); diff --git a/src/supertux/screen_manager.hpp b/src/supertux/screen_manager.hpp index a0a221c698..331e2c3d00 100644 --- a/src/supertux/screen_manager.hpp +++ b/src/supertux/screen_manager.hpp @@ -43,6 +43,7 @@ class VideoSystem; class ScreenManager final : public Currenton { public: + using callback_t = std::function; ScreenManager(VideoSystem& video_system, InputManager& input_manager); ~ScreenManager() override; @@ -56,6 +57,7 @@ class ScreenManager final : public Currenton // push new screen on screen_stack void push_screen(std::unique_ptr screen, std::unique_ptr fade = {}); + void push_screen(callback_t callback, std::unique_ptr fade = {}); void pop_screen(std::unique_ptr fade = {}); void set_screen_fade(std::unique_ptr fade); @@ -91,11 +93,14 @@ class ScreenManager final : public Currenton enum Type { PUSH_ACTION, POP_ACTION, QUIT_ACTION }; Type type; std::unique_ptr screen; - + callback_t callback; + Action(Type type_, - std::unique_ptr screen_ = {}) : + std::unique_ptr screen_ = {}, + callback_t cb = nullptr) : type(type_), - screen(std::move(screen_)) + screen(std::move(screen_)), + callback(std::move(cb)) {} }; diff --git a/src/supertux/sector.cpp b/src/supertux/sector.cpp index 21b1a1d284..1f7f99d19b 100644 --- a/src/supertux/sector.cpp +++ b/src/supertux/sector.cpp @@ -79,6 +79,19 @@ Sector::Sector(Level& parent) : SoundManager::current()->preload("sounds/shoot.wav"); } +Sector::Sector(Sector* sector) : + Base::Sector(sector), + m_level(sector->m_level), + m_text_object(add("Text")), + m_foremost_layer(sector->m_foremost_layer), + m_foremost_opaque_layer(sector->m_foremost_opaque_layer), + m_gravity(sector->m_gravity), + m_collision_system(sector->m_collision_system.get()) +{ + +} + + Sector::~Sector() { try @@ -237,9 +250,6 @@ Sector::activate(const Vector& player_pos) // The Sector object is called 'settings' as it is accessed as 'sector.settings' m_squirrel_environment->expose(*this, "settings"); - if (Editor::is_active()) - return; - // two-player hack: move other players to main player's position // Maybe specify 2 spawnpoints in the level? const auto players = get_objects_by_type(); diff --git a/src/supertux/sector.hpp b/src/supertux/sector.hpp index c6070f75b0..e9f88e8107 100644 --- a/src/supertux/sector.hpp +++ b/src/supertux/sector.hpp @@ -75,6 +75,7 @@ class Sector final : public Base::Sector public: Sector(Level& parent); + Sector(Sector* sector); ~Sector() override; void finish_construction(bool editable) override; diff --git a/src/supertux/sector_base.cpp b/src/supertux/sector_base.cpp index 394e9ef182..1ccb702fbc 100644 --- a/src/supertux/sector_base.cpp +++ b/src/supertux/sector_base.cpp @@ -28,6 +28,14 @@ Sector::Sector(const std::string& type) : { } +Sector::Sector(Sector* sector) : + GameObjectManager(sector), + m_name(sector->m_name), + m_init_script(sector->m_init_script), + m_squirrel_environment(sector->m_squirrel_environment) +{ +} + void Sector::finish_construction(bool) { diff --git a/src/supertux/sector_base.hpp b/src/supertux/sector_base.hpp index 6fb2d776e6..b26694ab71 100644 --- a/src/supertux/sector_base.hpp +++ b/src/supertux/sector_base.hpp @@ -30,6 +30,7 @@ class Sector : public GameObjectManager { public: Sector(const std::string& type); + Sector(Sector* sector); /** Needs to be called after parsing to finish the construction of the Sector before using it. */ @@ -55,7 +56,7 @@ class Sector : public GameObjectManager std::string m_name; std::string m_init_script; - std::unique_ptr m_squirrel_environment; + std::shared_ptr m_squirrel_environment; private: Sector(const Sector&) = delete; diff --git a/src/supertux/tile.cpp b/src/supertux/tile.cpp index 189cb8bebc..5f1232dfda 100644 --- a/src/supertux/tile.cpp +++ b/src/supertux/tile.cpp @@ -18,8 +18,10 @@ #include "supertux/tile.hpp" +#include "editor/editor.hpp" #include "math/aatriangle.hpp" #include "supertux/constants.hpp" +#include "supertux/gameconfig.hpp" #include "supertux/globals.hpp" #include "util/log.hpp" #include "video/drawing_context.hpp" @@ -208,10 +210,12 @@ Tile::draw_debug(Canvas& canvas, const Vector& pos, int z_pos, const Color& colo SurfacePtr Tile::get_current_surface() const { - if (m_images.size() > 1) { + // Check for editor's "Render animations" setting in case we call this method from the `get_current_editor_surface` method. + auto display_animations = (Editor::is_active() && g_config->editor_render_animations || !Editor::is_active()); + if (display_animations && m_images.size() > 1) { size_t frame = size_t(g_game_time * m_fps) % m_images.size(); return m_images[frame]; - } else if (m_images.size() == 1) { + } else if (Editor::is_active() || m_images.size() == 1) { return m_images[0]; } else { return {}; @@ -221,7 +225,7 @@ Tile::get_current_surface() const SurfacePtr Tile::get_current_editor_surface() const { - if (m_editor_images.size() > 1) { + if (g_config->editor_render_animations && m_editor_images.size() > 1) { size_t frame = size_t(g_game_time * m_fps) % m_editor_images.size(); return m_editor_images[frame]; } else if (m_editor_images.size() == 1) { diff --git a/src/trigger/door.cpp b/src/trigger/door.cpp index 7a2b9c9461..525193a65f 100644 --- a/src/trigger/door.cpp +++ b/src/trigger/door.cpp @@ -76,7 +76,7 @@ Door::get_settings() { ObjectSettings result = SpritedTrigger::get_settings(); - result.add_script(_("Script"), &m_script, "script"); + result.add_script(get_uid(), _("Script"), &m_script, "script"); result.add_text(_("Sector"), &m_target_sector, "sector"); result.add_text(_("Spawn point"), &m_target_spawnpoint, "spawnpoint"); result.add_bool(_("Locked?"), &m_locked, "locked"); diff --git a/src/trigger/scripttrigger.cpp b/src/trigger/scripttrigger.cpp index d200d8a6be..3f15fa9f5a 100644 --- a/src/trigger/scripttrigger.cpp +++ b/src/trigger/scripttrigger.cpp @@ -48,7 +48,7 @@ ScriptTrigger::get_settings() { ObjectSettings result = Trigger::get_settings(); - result.add_script(_("Script"), &script, "script"); + result.add_script(get_uid(), _("Script"), &script, "script"); result.add_bool(_("Button"), &must_activate, "button"); result.add_bool(_("Oneshot"), &oneshot, "oneshot", false); diff --git a/src/trigger/secretarea_trigger.cpp b/src/trigger/secretarea_trigger.cpp index 847860737d..77ae34a5d2 100644 --- a/src/trigger/secretarea_trigger.cpp +++ b/src/trigger/secretarea_trigger.cpp @@ -55,7 +55,7 @@ SecretAreaTrigger::get_settings() result.add_text(_("Name"), &m_name); result.add_text(_("Fade tilemap"), &fade_tilemap, "fade-tilemap"); result.add_translatable_text(_("Message"), &message, "message"); - result.add_script(_("Script"), &script, "script"); + result.add_script(get_uid(), _("Script"), &script, "script"); result.reorder({"fade-tilemap", "script", "sprite", "message", "width", "height", "name", "x", "y"}); diff --git a/src/trigger/switch.cpp b/src/trigger/switch.cpp index 7976b7d656..6bb622483d 100644 --- a/src/trigger/switch.cpp +++ b/src/trigger/switch.cpp @@ -63,8 +63,8 @@ Switch::get_settings() result.add_direction(_("Direction"), &m_dir, { Direction::NONE, Direction::LEFT, Direction::RIGHT, Direction::UP, Direction::DOWN }, "direction"); - result.add_script(_("Turn on script"), &m_script, "script"); - result.add_script(_("Turn off script"), &m_off_script, "off-script"); + result.add_script(get_uid(), _("Turn on script"), &m_script, "script"); + result.add_script(get_uid(), _("Turn off script"), &m_off_script, "off-script"); result.reorder({"direction", "script", "off-script", "sticky", "sprite", "x", "y"}); diff --git a/src/util/fade_helper.cpp b/src/util/fade_helper.cpp index 59f0ee0c71..3cc6d8a3d8 100644 --- a/src/util/fade_helper.cpp +++ b/src/util/fade_helper.cpp @@ -40,6 +40,17 @@ FadeHelper::FadeHelper(float* value, float time, { } +FadeHelper::FadeHelper(FadeHelper* other) : + m_value(other->m_value), + m_progress(other->m_progress), + m_start(other->m_start), + m_target(other->m_target), + m_time(other->m_time), + m_total_time(other->m_total_time), + m_ease(other->m_ease) +{ +} + float FadeHelper::update(float dt_sec) { diff --git a/src/util/fade_helper.hpp b/src/util/fade_helper.hpp index cf87d4e9de..6496450bc5 100644 --- a/src/util/fade_helper.hpp +++ b/src/util/fade_helper.hpp @@ -29,7 +29,8 @@ class FadeHelper FadeHelper(float* value, float time, float target_value, easing ease = LinearInterpolation); - + FadeHelper(FadeHelper* other); + /** * Increases the internal timer of the FadeHelper. * diff --git a/src/util/file_system.cpp b/src/util/file_system.cpp index 030c11836f..da9028441f 100644 --- a/src/util/file_system.cpp +++ b/src/util/file_system.cpp @@ -15,12 +15,17 @@ // along with this program. If not, see . #include "util/file_system.hpp" +#include "supertux/globals.hpp" #include +#include #include #include #include #include +#ifndef _WIN32 +#include +#endif #include #if defined(_WIN32) #include @@ -43,6 +48,7 @@ #include "gui/dialog.hpp" #include "util/log.hpp" #include "util/string_util.hpp" +#include "supertux/gameconfig.hpp" namespace fs = std::filesystem; @@ -78,6 +84,15 @@ void copy(const std::string& source_path, const std::string& target_path) fs::copy_file(source_path, target_path, fs::copy_options::overwrite_existing); } +std::string strip_leading_dirs(std::string filename) +{ + while (filename.size() > 0 && (filename[filename.size()-1] == '/' || filename[filename.size()-1] == '\\')) + { + filename.pop_back(); + } + return filename; +} + std::string dirname(const std::string& filename) { std::string::size_type p = filename.find_last_of('/'); @@ -89,8 +104,11 @@ std::string dirname(const std::string& filename) return filename.substr(0, p+1); } -std::string basename(const std::string& filename) +std::string basename(std::string filename, bool greedy) { + if (greedy) + filename = strip_leading_dirs(filename); + std::string::size_type p = filename.find_last_of('/'); if (p == std::string::npos) p = filename.find_last_of('\\'); @@ -235,6 +253,34 @@ void open_path(const std::string& path) #endif } +void +open_editor(const std::string& filename) +{ + const char* default_editor = +# if defined(_WIN32) || defined(_WIN64) + nullptr; +# else + getenv("EDITOR"); +# endif + std::string cmd; + if (!g_config->preferred_text_editor.empty()) + cmd = g_config->preferred_text_editor + " \"" + filename + "\" &"; + else if (default_editor) + { + cmd = std::string(default_editor) + " \"" + filename + "\" &"; + } + + int ret = system(cmd.c_str()); + if (ret < 0) + { + log_fatal << "failed to spawn editor: " << cmd << std::endl; + } + else if (ret > 0) + { + log_fatal << "error " << ret << " while executing: " << cmd << std::endl; + } +} + std::string escape_url(const std::string& url) { #ifndef __EMSCRIPTEN__ diff --git a/src/util/file_system.hpp b/src/util/file_system.hpp index ba6bf77200..42d7a0ba72 100644 --- a/src/util/file_system.hpp +++ b/src/util/file_system.hpp @@ -35,8 +35,12 @@ void copy(const std::string& source_path, const std::string& target_path); /** returns the path of the directory the file is in */ std::string dirname(const std::string& filename); -/** returns the name of the file */ -std::string basename(const std::string& filename); +/** returns the name of the file + * @param filename The path to get the basename from. + * @param greedy If true, then attempt to strip any slashes from the end first. + * This fixes situations like /some/dir/ where they really meant /some/dir. + */ +std::string basename(std::string path, bool greedy = false); /** Return a path to 'filename' that is relative to 'basedir', e.g. reldir("/levels/juser/level1.stl", "/levels") -> "juser/level1.stl" */ @@ -48,6 +52,9 @@ std::string extension(const std::string& filename); /** remove everything starting from and including the last dot */ std::string strip_extension(const std::string& filename); +/** strip any leading paths, like /some/path///// */ +std::string strip_leading_dirs(std::string filename); + /** normalize filename so that "blup/bla/blo/../../bar" will become "blup/bar" */ std::string normalize(const std::string& filename); @@ -66,6 +73,11 @@ std::string escape_url(const std::string& url); * @param path path to open */ void open_path(const std::string& path); + +/** Opens a file in the users preferred text editor. + * @param filename File to edit + */ +void open_editor(const std::string& filename); /** Opens an URL in the user's preferred browser. * @param url URL to open diff --git a/src/util/file_watcher.cpp b/src/util/file_watcher.cpp new file mode 100644 index 0000000000..814f8c887d --- /dev/null +++ b/src/util/file_watcher.cpp @@ -0,0 +1,59 @@ +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include "util/file_watcher.hpp" + +FileWatcher::FileWatcher() +{ +} + +time_t +FileWatcher::get_mtime(const std::string &filename) +{ + struct stat file_stat; + if (stat(filename.c_str(), &file_stat) != 0) + { + // TODO + return 0; + } + + return file_stat.st_mtime; +} + +void +FileWatcher::start_monitoring(std::string filename, FileWatcher::callback_t fun) +{ + m_files.emplace(std::move(filename), FileWatcher::FileInfo{filename, get_mtime(filename), fun}); +} + +void +FileWatcher::poll() +{ + struct stat file_stat; + for (auto &file : m_files) + { + FileInfo &finfo = file.second; + time_t mtime = get_mtime(file.first); + + // Same time, don't bother + if (finfo.m_last_mtime == mtime) + continue; + + finfo.callback(finfo); + finfo.m_last_mtime = mtime; + } +} diff --git a/src/util/file_watcher.hpp b/src/util/file_watcher.hpp new file mode 100644 index 0000000000..4478b2cd24 --- /dev/null +++ b/src/util/file_watcher.hpp @@ -0,0 +1,53 @@ +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include +#include +#include +#include + +class FileWatcher +{ +public: + struct FileInfo; + using callback_t = std::function; + + struct FileInfo { + std::string filename; + time_t m_last_mtime; + callback_t callback; + + bool operator==(struct FileInfo& other) const { + return other.filename == filename; + } + bool operator==(const std::string& other) const { + return other == filename; + } + }; +public: + FileWatcher(); + ~FileWatcher() = default; + + void start_monitoring(std::string filename, callback_t fun); + + void poll(); + time_t get_mtime(const std::string &filename); +private: + + std::unordered_map m_files; +}; diff --git a/src/util/script_manager.cpp b/src/util/script_manager.cpp new file mode 100644 index 0000000000..8ed09bf0ca --- /dev/null +++ b/src/util/script_manager.cpp @@ -0,0 +1,94 @@ +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "script_manager.hpp" +#include "util/file_system.hpp" +#include "util/file_watcher.hpp" +#include +#include +#include +#include + +ScriptManager::ScriptManager() : + m_scripts(), + m_watcher() +{ + +} + +bool +ScriptManager::is_script_registered(UID key) +{ + auto res = std::find(m_scripts.begin(), m_scripts.end(), key); + return res != m_scripts.end(); +} + +std::string +ScriptManager::full_filename_from_key(UID key) +{ + return FileSystem::join(PHYSFS_getWriteDir(), "tmp/" + filename_from_key(key)); +} + +std::string +ScriptManager::filename_from_key(UID key) +{ + return "script_" + std::to_string(key.get_value()) + ".nut"; +} + +time_t +ScriptManager::get_mtime(UID key) +{ + return m_watcher.get_mtime(full_filename_from_key(key)); +} + +void +ScriptManager::register_script(UID key, std::string* script) +{ + std::string full_filename = full_filename_from_key(key); + + // TODO: Check if it's different. Causes some editors to nag. + // Write current script contents + std::ofstream file; + file.open(full_filename); + file << *script; + file.close(); + + if (is_script_registered(key)) + return; + m_scripts.push_back({key, script}); + + m_watcher.start_monitoring(full_filename, [this, key](FileWatcher::FileInfo& file) { + auto res = std::find(m_scripts.begin(), m_scripts.end(), key); + if (res == m_scripts.end()) + return; + + *res->script = ""; + std::fstream readme; + readme.open(file.filename); + std::string line; + while (std::getline(readme, line)) + { + *res->script += line + "\n"; + } + readme.close(); + }); +} + +void +ScriptManager::poll() +{ + m_watcher.poll(); +} diff --git a/src/util/script_manager.hpp b/src/util/script_manager.hpp new file mode 100644 index 0000000000..9e95134450 --- /dev/null +++ b/src/util/script_manager.hpp @@ -0,0 +1,58 @@ +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include "util/file_watcher.hpp" +#include "util/uid.hpp" +#include +#include +#include + +class ScriptManager +{ +public: + using callback_t = std::function; + + struct ScriptInfo { + UID key; + std::string* script; + + bool operator==(struct ScriptInfo& other) const { + return other.key == key; + } + bool operator==(const UID& other) const { + return other == key; + } + }; +public: + ScriptManager(); + ~ScriptManager() = default; + + static std::string filename_from_key(UID key); + static std::string full_filename_from_key(UID key); + + time_t get_mtime(UID key); + + bool is_script_registered(UID key); + void register_script(UID key, std::string* script); + + void poll(); +private: + + std::vector m_scripts; + FileWatcher m_watcher; +}; diff --git a/src/util/uid.hpp b/src/util/uid.hpp index a93f3ff259..8889bd35c8 100644 --- a/src/util/uid.hpp +++ b/src/util/uid.hpp @@ -74,6 +74,8 @@ class UID inline bool operator!=(const UID& other) const { return m_value != other.m_value; } + + inline uint32_t get_value() const { return m_value; } inline Magic get_magic() const { return static_cast((m_value & 0xffff0000u) >> 16); } diff --git a/src/video/canvas.cpp b/src/video/canvas.cpp index 103bdbfb5c..b2870fcb0a 100644 --- a/src/video/canvas.cpp +++ b/src/video/canvas.cpp @@ -32,7 +32,8 @@ Canvas::Canvas(DrawingContext& context, obstack& obst) : m_context(context), m_obst(obst), - m_requests() + m_requests(), + m_blur(0) { m_requests.reserve(500); } @@ -282,6 +283,7 @@ Canvas::draw_filled_rect(const Rectf& rect, const Color& color, float radius, in request->color = color; request->color.alpha = color.alpha * m_context.transform().alpha; request->radius = radius; + request->blur = m_blur; m_requests.push_back(request); } diff --git a/src/video/canvas.hpp b/src/video/canvas.hpp index 0e33019d57..948d8d0a80 100644 --- a/src/video/canvas.hpp +++ b/src/video/canvas.hpp @@ -47,7 +47,7 @@ class Canvas final public: Canvas(DrawingContext& context, obstack& obst); ~Canvas(); - + void draw_surface(const SurfacePtr& surface, const Vector& position, int layer); void draw_surface(const SurfacePtr& surface, const Vector& position, float angle, const Color& color, const Blend& blend, int layer); @@ -96,6 +96,8 @@ class Canvas final void clear(); void render(Renderer& renderer, Filter filter); + + void set_blur(int blur) { m_blur = blur; } inline DrawingContext& get_context() { return m_context; } @@ -106,6 +108,7 @@ class Canvas final private: DrawingContext& m_context; obstack& m_obst; + int m_blur; std::vector m_requests; private: diff --git a/src/video/drawing_request.hpp b/src/video/drawing_request.hpp index a34aaafa26..0f3b916665 100644 --- a/src/video/drawing_request.hpp +++ b/src/video/drawing_request.hpp @@ -110,7 +110,8 @@ struct FillRectRequest : public DrawingRequest DrawingRequest(transform), rect(), color(), - radius() + radius(), + blur() {} RequestType get_type() const override { return RequestType::FILLRECT; } @@ -118,6 +119,7 @@ struct FillRectRequest : public DrawingRequest Rectf rect; Color color; float radius; + int blur; }; struct InverseEllipseRequest : public DrawingRequest diff --git a/src/video/gl/gl20_context.cpp b/src/video/gl/gl20_context.cpp index 11e24c795f..d8020fcd3a 100644 --- a/src/video/gl/gl20_context.cpp +++ b/src/video/gl/gl20_context.cpp @@ -135,6 +135,9 @@ GL20Context::set_color(const Color& color) assert_gl(); } +void +GL20Context::set_blur(int amount) {} + void GL20Context::bind_texture(const Texture& texture, const Texture* displacement_texture) { diff --git a/src/video/gl/gl20_context.hpp b/src/video/gl/gl20_context.hpp index d1d7b0909a..745abce718 100644 --- a/src/video/gl/gl20_context.hpp +++ b/src/video/gl/gl20_context.hpp @@ -41,6 +41,8 @@ class GL20Context final : public GLContext virtual void set_colors(const float* data, size_t size) override; virtual void set_color(const Color& color) override; + + virtual void set_blur(int amount) override; virtual void bind_texture(const Texture& texture, const Texture* displacement_texture) override; virtual void bind_no_texture() override; diff --git a/src/video/gl/gl33core_context.cpp b/src/video/gl/gl33core_context.cpp index 928c917f00..ca4d242beb 100644 --- a/src/video/gl/gl33core_context.cpp +++ b/src/video/gl/gl33core_context.cpp @@ -34,7 +34,8 @@ GL33CoreContext::GL33CoreContext(GLVideoSystem& video_system) : m_white_texture(), m_black_texture(), m_grey_texture(), - m_transparent_texture() + m_transparent_texture(), + m_blur() { assert_gl(); @@ -176,6 +177,12 @@ GL33CoreContext::set_color(const Color& color) m_vertex_arrays->set_color(color); } +void +GL33CoreContext::set_blur(int amount) +{ + m_blur = amount; +} + void GL33CoreContext::bind_texture(const Texture& texture, const Texture* displacement_texture) { @@ -183,6 +190,7 @@ GL33CoreContext::bind_texture(const Texture& texture, const Texture* displacemen GLTextureRenderer* back_renderer = static_cast(m_video_system.get_back_renderer()); + glUniform1i(m_program->get_blur_location(), m_blur); if (displacement_texture && back_renderer->is_rendering()) { glActiveTexture(GL_TEXTURE0); @@ -231,6 +239,7 @@ GL33CoreContext::bind_no_texture() { assert_gl(); + glUniform1i(m_program->get_blur_location(), m_blur); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, m_white_texture->get_handle()); diff --git a/src/video/gl/gl33core_context.hpp b/src/video/gl/gl33core_context.hpp index 529cca8cf1..808854c34e 100644 --- a/src/video/gl/gl33core_context.hpp +++ b/src/video/gl/gl33core_context.hpp @@ -46,6 +46,8 @@ class GL33CoreContext final : public GLContext virtual void set_colors(const float* data, size_t size) override; virtual void set_color(const Color& color) override; + + virtual void set_blur(int amount) override; virtual void bind_texture(const Texture& texture, const Texture* displacement_texture) override; virtual void bind_no_texture() override; @@ -65,6 +67,7 @@ class GL33CoreContext final : public GLContext std::unique_ptr m_black_texture; std::unique_ptr m_grey_texture; std::unique_ptr m_transparent_texture; + int m_blur; private: GL33CoreContext(const GL33CoreContext&) = delete; diff --git a/src/video/gl/gl_context.hpp b/src/video/gl/gl_context.hpp index c55bf56ba3..ab95bd89ae 100644 --- a/src/video/gl/gl_context.hpp +++ b/src/video/gl/gl_context.hpp @@ -38,6 +38,8 @@ class GLContext virtual void ortho(float width, float height, bool vflip) = 0; virtual void blend_func(GLenum src, GLenum dst) = 0; + + virtual void set_blur(int blur) = 0; virtual void set_positions(const float* data, size_t size) = 0; diff --git a/src/video/gl/gl_painter.cpp b/src/video/gl/gl_painter.cpp index 382606c3cd..a43e56d945 100644 --- a/src/video/gl/gl_painter.cpp +++ b/src/video/gl/gl_painter.cpp @@ -237,6 +237,7 @@ GLPainter::draw_filled_rect(const FillRectRequest& request) GLContext& context = m_video_system.get_context(); + context.set_blur(request.blur); context.blend_func(sfactor(request.blend), dfactor(request.blend)); context.bind_no_texture(); context.set_texcoord(0.0f, 0.0f); @@ -307,6 +308,7 @@ GLPainter::draw_filled_rect(const FillRectRequest& request) context.draw_arrays(GL_TRIANGLE_FAN, 0, 4); } + context.set_blur(0); assert_gl(); } diff --git a/src/video/gl/gl_program.cpp b/src/video/gl/gl_program.cpp index 39ab329b25..4a7a7b79e5 100644 --- a/src/video/gl/gl_program.cpp +++ b/src/video/gl/gl_program.cpp @@ -74,6 +74,7 @@ GLProgram::GLProgram() : m_animate_location = glGetUniformLocation(m_program, "animate"); m_displacement_animate_location = glGetUniformLocation(m_program, "displacement_animate"); m_is_displacement_location = glGetUniformLocation(m_program, "is_displacement"); + m_blur_location = glGetUniformLocation(m_program, "blur"); m_position_location = glGetAttribLocation(m_program, "position"); m_texcoord_location = glGetAttribLocation(m_program, "texcoord"); m_diffuse_location = glGetAttribLocation(m_program, "diffuse"); diff --git a/src/video/gl/gl_program.hpp b/src/video/gl/gl_program.hpp index 7fb16163cd..f29bd06c0d 100644 --- a/src/video/gl/gl_program.hpp +++ b/src/video/gl/gl_program.hpp @@ -46,6 +46,7 @@ class GLProgram final inline GLint get_texcoord_location() const { return check_valid(m_texcoord_location, "texcoord"); } inline GLint get_diffuse_location() const { return check_valid(m_diffuse_location, "diffuse"); } inline GLint get_is_displacement_location() const { return check_valid(m_is_displacement_location, "is_displacement"); } + inline GLint get_blur_location() const { return check_valid(m_blur_location, "blur"); } private: bool get_link_status() const; @@ -72,6 +73,7 @@ class GLProgram final GLint m_texcoord_location; GLint m_diffuse_location; GLint m_is_displacement_location; + GLint m_blur_location; private: GLProgram(const GLProgram&) = delete; diff --git a/src/worldmap/level_tile.cpp b/src/worldmap/level_tile.cpp index 1684238bbd..516368beab 100644 --- a/src/worldmap/level_tile.cpp +++ b/src/worldmap/level_tile.cpp @@ -151,7 +151,7 @@ LevelTile::get_settings() ObjectSettings result = WorldMapObject::get_settings(); result.add_level(_("Level"), &m_level_filename, "level", basedir); - result.add_script(_("Outro script"), &m_extro_script, "extro-script"); + result.add_script(get_uid(), _("Outro script"), &m_extro_script, "extro-script"); result.add_bool(_("Auto play"), &m_auto_play, "auto-play", false); result.add_color(_("Title colour"), &m_title_color, "color", Color::WHITE); diff --git a/src/worldmap/spawn_point.cpp b/src/worldmap/spawn_point.cpp index 8598b14501..3bbc2d1327 100644 --- a/src/worldmap/spawn_point.cpp +++ b/src/worldmap/spawn_point.cpp @@ -80,7 +80,7 @@ SpawnPointObject::get_settings() result.reorder({"auto-dir", "name", "x", "y"}); - result.add_test_from_here(); + result.add_test_from_here(this); return result; } diff --git a/src/worldmap/special_tile.cpp b/src/worldmap/special_tile.cpp index 8c309c7aaa..c4413254b0 100644 --- a/src/worldmap/special_tile.cpp +++ b/src/worldmap/special_tile.cpp @@ -73,7 +73,7 @@ SpecialTile::get_settings() result.add_translatable_text(_("Message"), &m_map_message, "map-message"); result.add_bool(_("Show message"), &m_passive_message, "passive-message", false); - result.add_script(_("Script"), &m_script, "script"); + result.add_script(get_uid(), _("Script"), &m_script, "script"); result.add_bool(_("Invisible"), &m_invisible, "invisible-tile", false); result.add_text(_("Direction"), &m_apply_direction, "apply-to-direction", "north-east-south-west"); diff --git a/src/worldmap/worldmap.cpp b/src/worldmap/worldmap.cpp index 271064d927..0b87da939b 100644 --- a/src/worldmap/worldmap.cpp +++ b/src/worldmap/worldmap.cpp @@ -61,6 +61,7 @@ WorldMap::WorldMap(const std::string& filename, Savegame& savegame, m_passive_message_timer(), m_allow_item_pocket(true), m_enter_level(false), + m_really_enter_level(false), m_in_level(false), m_in_world_select(false), m_next_filename(), @@ -158,8 +159,13 @@ void WorldMap::update(float dt_sec, const Controller& controller) { if (m_in_world_select) return; - + process_input(controller); + + if (m_really_enter_level) + { + m_enter_level = true; + } if (m_in_level) return; if (MenuManager::instance().is_active()) return; @@ -177,7 +183,8 @@ WorldMap::update(float dt_sec, const Controller& controller) void WorldMap::process_input(const Controller& controller) { - m_enter_level = false; + if (!m_really_enter_level) + m_enter_level = false; if (controller.pressed(Control::ACTION) && !m_in_level) { @@ -359,5 +366,4 @@ WorldMap::get_filename() const { return m_map_filename; } - } // namespace worldmap diff --git a/src/worldmap/worldmap.hpp b/src/worldmap/worldmap.hpp index ad4782e956..c80e4e4a86 100644 --- a/src/worldmap/worldmap.hpp +++ b/src/worldmap/worldmap.hpp @@ -93,6 +93,8 @@ class WorldMap final : public Screen, bool perform_full_setup = true); const std::string& get_filename() const; + + inline void start_level() { m_really_enter_level = true; } bool is_item_pocket_allowed() const { return m_allow_item_pocket; } @@ -125,6 +127,7 @@ class WorldMap final : public Screen, Timer m_passive_message_timer; bool m_allow_item_pocket; + bool m_really_enter_level; bool m_enter_level; bool m_in_level; bool m_in_world_select; diff --git a/src/worldmap/worldmap_object.cpp b/src/worldmap/worldmap_object.cpp index 2bf0a1dde3..f2f629f92b 100644 --- a/src/worldmap/worldmap_object.cpp +++ b/src/worldmap/worldmap_object.cpp @@ -72,8 +72,8 @@ WorldMapObject::get_settings() result.remove("x"); result.remove("y"); - result.add_int(_("X"), &m_tile_x, "x", {}, OPTION_HIDDEN); - result.add_int(_("Y"), &m_tile_y, "y", {}, OPTION_HIDDEN); + result.add_int(_("X"), &m_tile_x, "x", {}, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); + result.add_int(_("Y"), &m_tile_y, "y", {}, OPTION_HIDDEN | OPTION_VISIBLE_PROPERTIES); return result; } diff --git a/src/worldmap/worldmap_sector.cpp b/src/worldmap/worldmap_sector.cpp index 07dad63343..94901d84f3 100644 --- a/src/worldmap/worldmap_sector.cpp +++ b/src/worldmap/worldmap_sector.cpp @@ -356,6 +356,11 @@ WorldMapSector::update(float dt_sec) std::string levelfile = m_parent.m_levels_path + level_->get_level_filename(); auto game_session = std::make_unique(levelfile, m_parent.get_savegame(), &level_->get_statistics()); + if (m_parent.m_really_enter_level) + { + game_session->skip_intro(); + m_parent.m_really_enter_level = false; + } game_session->restart_level(); // update state and savegame