From 525474378a27444877760a0fcbd3c5d690d0d402 Mon Sep 17 00:00:00 2001 From: Semphris Date: Sat, 12 Sep 2020 15:52:48 -0400 Subject: [PATCH 1/9] Introducing customisable particles (not complete in this commit, but in a working state) --- src/object/cloud_particle_system.cpp | 205 ++++++++- src/object/cloud_particle_system.hpp | 38 +- src/object/particlesystem.cpp | 7 +- src/object/particlesystem.hpp | 8 +- src/object/particlesystem_interactive.cpp | 6 +- src/object/rain_particle_system.cpp | 232 +++++++++- src/object/rain_particle_system.hpp | 46 +- src/scripting/clouds.cpp | 54 +++ src/scripting/clouds.hpp | 60 +++ src/scripting/rain.cpp | 55 +++ src/scripting/rain.hpp | 60 +++ src/scripting/wrapper.cpp | 530 ++++++++++++++++++++++ src/scripting/wrapper.hpp | 4 + src/scripting/wrapper.interface.hpp | 2 + src/video/surface_batch.cpp | 3 +- src/video/surface_batch.hpp | 5 +- 16 files changed, 1273 insertions(+), 42 deletions(-) create mode 100644 src/scripting/clouds.cpp create mode 100644 src/scripting/clouds.hpp create mode 100644 src/scripting/rain.cpp create mode 100644 src/scripting/rain.hpp diff --git a/src/object/cloud_particle_system.cpp b/src/object/cloud_particle_system.cpp index 076f46c549d..7371b49c8f5 100644 --- a/src/object/cloud_particle_system.cpp +++ b/src/object/cloud_particle_system.cpp @@ -17,18 +17,40 @@ #include "object/cloud_particle_system.hpp" #include "math/random.hpp" +#include "video/drawing_context.hpp" #include "video/surface.hpp" +#include "video/surface_batch.hpp" CloudParticleSystem::CloudParticleSystem() : ParticleSystem(128), - cloudimage(Surface::from_file("images/particles/cloud.png")) + ExposedObject(this), + cloudimage(Surface::from_file("images/particles/cloud.png")), + + m_current_speed(1.f), + m_target_speed(1.f), + m_speed_fade_time_remaining(0.f), + + m_current_amount(15.f), + m_target_amount(15.f), + m_amount_fade_time_remaining(0.f), + m_current_real_amount(0) { init(); } CloudParticleSystem::CloudParticleSystem(const ReaderMapping& reader) : ParticleSystem(reader, 128), - cloudimage(Surface::from_file("images/particles/cloud.png")) + ExposedObject(this), + cloudimage(Surface::from_file("images/particles/cloud.png")), + + m_current_speed(1.f), + m_target_speed(1.f), + m_speed_fade_time_remaining(0.f), + + m_current_amount(15.f), + m_target_amount(15.f), + m_amount_fade_time_remaining(0.f), + m_current_real_amount(0) { init(); } @@ -42,28 +64,193 @@ void CloudParticleSystem::init() virtual_width = 2000.0; // create some random clouds - for (size_t i=0; i<15; ++i) { + add_clouds(15, 0.f); +} + +ObjectSettings CloudParticleSystem::get_settings() +{ + ObjectSettings result = ParticleSystem::get_settings(); + + result.add_float(_("Intensity"), &m_current_amount, "intensity", 15.f); + + result.reorder({"intensity", "enabled", "name"}); + + return result; +} + + +void CloudParticleSystem::update(float dt_sec) +{ + if (!enabled) + return; + + // Update speed + if (m_speed_fade_time_remaining > 0.f) { + if (dt_sec >= m_speed_fade_time_remaining) { + m_current_speed = m_target_speed; + m_speed_fade_time_remaining = 0.f; + } else { + float amount = dt_sec / m_speed_fade_time_remaining; + m_current_speed += (m_target_speed - m_current_speed) * amount; + m_speed_fade_time_remaining -= dt_sec; + } + } + + for (auto& particle : particles) { + auto cloudParticle = dynamic_cast(particle.get()); + if (!cloudParticle) + continue; + cloudParticle->pos.x += cloudParticle->speed * dt_sec * m_current_speed; + + // Update alpha + if (cloudParticle->target_time_remaining > 0.f) { + if (dt_sec >= cloudParticle->target_time_remaining) { + cloudParticle->alpha = cloudParticle->target_alpha; + cloudParticle->target_time_remaining = 0.f; + if (cloudParticle->alpha == 0.f) { + // Remove this particle + // But not right here, else it'd mess with the iterator + } + } else { + float amount = dt_sec / cloudParticle->target_time_remaining; + cloudParticle->alpha += (cloudParticle->target_alpha - cloudParticle->alpha) * amount; + cloudParticle->target_time_remaining -= dt_sec; + } + } + } + + // Clear dead clouds + // Scroll through the vector backwards, because removing an element affects + // the index of all elements after it (prevents buggy behavior) + for (int i = static_cast(particles.size()) - 1; i >= 0; --i) { + auto particle = dynamic_cast(particles.at(i).get()); + + if (particle->target_alpha == 0.f && particle->target_time_remaining == 0.f) + particles.erase(particles.begin()+i); + } +} + +int CloudParticleSystem::add_clouds(int amount, float fade_time) +{ + int target_amount = m_current_real_amount + amount; + + if (target_amount > max_amount) + target_amount = max_amount; + + int amount_to_add = target_amount - m_current_real_amount; + + for (int i = 0; i < amount_to_add; ++i) { auto particle = std::make_unique(); particle->pos.x = graphicsRandom.randf(virtual_width); particle->pos.y = graphicsRandom.randf(virtual_height); particle->texture = cloudimage; particle->speed = -graphicsRandom.randf(25.0, 54.0); + particle->alpha = (fade_time == 0.f) ? 1.f : 0.f; + particle->target_alpha = 1.f; + particle->target_time_remaining = fade_time; particles.push_back(std::move(particle)); } + + m_current_real_amount = target_amount; + return m_current_real_amount; } -void CloudParticleSystem::update(float dt_sec) +int CloudParticleSystem::remove_clouds(int amount, float fade_time) +{ + int target_amount = m_current_real_amount - amount; + + if (target_amount < min_amount) + target_amount = min_amount; + + int amount_to_remove = m_current_real_amount - target_amount; + + int i = 0; + for (; i < amount_to_remove && i < static_cast(particles.size()); ++i) { + + auto particle = dynamic_cast(particles.at(i).get()); + if (particle->target_alpha != 1.f || particle->target_time_remaining != 0.f) { + // Skip that one, it doesn't count + --i; + } else { + particle->target_alpha = 0.f; + particle->target_time_remaining = fade_time; + } + } + + return i; +} + +void CloudParticleSystem::fade_speed(float new_speed, float fade_time) +{ + // No check to enabled; change the fading even if it's disabled + + // If time is 0 (or smaller?), then update() will never change m_current_speed + if (fade_time <= 0.f) + { + m_current_speed = new_speed; + } + + m_target_speed = new_speed; + m_speed_fade_time_remaining = fade_time; +} + +void CloudParticleSystem::fade_amount(int new_amount, float fade_time, float time_between) +{ + // No check to enabled; change the fading even if it's disabled + + int delta = new_amount - m_current_real_amount; + + if (delta < 0) + { + remove_clouds(-delta, fade_time); + } + else if (delta > 0) + { + add_clouds(delta, fade_time); + } // Else delta == 0, in which case there is nothing to do +} + + +void CloudParticleSystem::draw(DrawingContext& context) { if (!enabled) return; - for (auto& particle : particles) { - auto cloudParticle = dynamic_cast(particle.get()); - if (!cloudParticle) - continue; - cloudParticle->pos.x += cloudParticle->speed * dt_sec; + context.push_transform(); + + std::unordered_map batches; + for (const auto& particle : particles) { + if (particle->alpha != 1.f) { + const auto& batch_it = batches.emplace( + particle->texture->clone(), + SurfaceBatch( + particle->texture, + Color(1.f, 1.f, 1.f, particle->alpha) + )); + batch_it.first->second.draw(particle->pos, particle->angle); + } else { + auto it = batches.find(particle->texture); + if (it == batches.end()) { + const auto& batch_it = batches.emplace(particle->texture, + SurfaceBatch(particle->texture)); + batch_it.first->second.draw(particle->pos, particle->angle); + } else { + it->second.draw(particle->pos, particle->angle); + } + } } + + for(auto& it : batches) { + auto& surface = it.first; + auto& batch = it.second; + // FIXME: What is the colour used for? + // RESOLVED : That's the tint and the alpha + context.color().draw_surface_batch(surface, batch.move_srcrects(), + batch.move_dstrects(), batch.move_angles(), batch.get_color(), z_pos); + } + + context.pop_transform(); } /* EOF */ diff --git a/src/object/cloud_particle_system.hpp b/src/object/cloud_particle_system.hpp index e04eaf33b7f..16e902de900 100644 --- a/src/object/cloud_particle_system.hpp +++ b/src/object/cloud_particle_system.hpp @@ -18,11 +18,14 @@ #define HEADER_SUPERTUX_OBJECT_CLOUD_PARTICLE_SYSTEM_HPP #include "object/particlesystem.hpp" +#include "scripting/clouds.hpp" #include "video/surface_ptr.hpp" class ReaderMapping; -class CloudParticleSystem final : public ParticleSystem +class CloudParticleSystem final : + public ParticleSystem, + public ExposedObject { public: CloudParticleSystem(); @@ -32,26 +35,57 @@ class CloudParticleSystem final : public ParticleSystem void init(); virtual void update(float dt_sec) override; + virtual void draw(DrawingContext& context) override; + virtual std::string get_class() const override { return "particles-clouds"; } virtual std::string get_display_name() const override { return _("Cloud Particles"); } + virtual ObjectSettings get_settings() override; virtual const std::string get_icon_path() const override { return "images/engine/editor/clouds.png"; } + void fade_speed(float new_speed, float fade_time); + void fade_amount(int new_amount, float fade_time, float time_between = 0.f); + + // Minimum and maximum multiplier for the amount of clouds + static int constexpr const max_amount = 500; + static int constexpr const min_amount = 0; + +private: + /** Returns the amount that got inserted (In case max_amount got hit) */ + int add_clouds(int amount, float fade_time); + + /** Returns the amount that got removed (In case min_amount got hit) */ + int remove_clouds(int amount, float fade_time); + private: class CloudParticle : public Particle { public: float speed; + float target_alpha; + float target_time_remaining; CloudParticle() : - speed() + speed(), + target_alpha(), + target_time_remaining() {} }; SurfacePtr cloudimage; + float m_current_speed; + float m_target_speed; + float m_speed_fade_time_remaining; + + float m_current_amount; + float m_target_amount; + float m_amount_fade_time_remaining; + + int m_current_real_amount; + private: CloudParticleSystem(const CloudParticleSystem&) = delete; CloudParticleSystem& operator=(const CloudParticleSystem&) = delete; diff --git a/src/object/particlesystem.cpp b/src/object/particlesystem.cpp index 047162f7f43..ad88777b37c 100644 --- a/src/object/particlesystem.cpp +++ b/src/object/particlesystem.cpp @@ -30,7 +30,7 @@ ParticleSystem::ParticleSystem(const ReaderMapping& reader, float max_particle_size_) : GameObject(reader), - ExposedObject(this), + //ExposedObject(this), max_particle_size(max_particle_size_), z_pos(LAYER_BACKGROUND1), particles(), @@ -44,7 +44,7 @@ ParticleSystem::ParticleSystem(const ReaderMapping& reader, float max_particle_s ParticleSystem::ParticleSystem(float max_particle_size_) : GameObject(), - ExposedObject(this), + //ExposedObject(this), max_particle_size(max_particle_size_), z_pos(LAYER_BACKGROUND1), particles(), @@ -118,7 +118,8 @@ ParticleSystem::draw(DrawingContext& context) batch.move_srcrects(), batch.move_dstrects(), batch.move_angles(), - Color::WHITE, z_pos); + batch.get_color(), + z_pos); } context.pop_transform(); diff --git a/src/object/particlesystem.hpp b/src/object/particlesystem.hpp index 5246ca7cb5d..08745058e00 100644 --- a/src/object/particlesystem.hpp +++ b/src/object/particlesystem.hpp @@ -43,8 +43,8 @@ class ReaderMapping; class, initialize particles in the constructor and move them in the simulate function. */ -class ParticleSystem : public GameObject, - public ExposedObject +class ParticleSystem : public GameObject//, + //public ExposedObject { public: ParticleSystem(const ReaderMapping& reader, float max_particle_size = 60); @@ -69,7 +69,8 @@ class ParticleSystem : public GameObject, Particle() : pos(), angle(), - texture() + texture(), + alpha() {} virtual ~Particle() @@ -79,6 +80,7 @@ class ParticleSystem : public GameObject, // angle at which to draw particle float angle; SurfacePtr texture; + float alpha; private: Particle(const Particle&) = delete; diff --git a/src/object/particlesystem_interactive.cpp b/src/object/particlesystem_interactive.cpp index efd47ec6ea9..340f2059005 100644 --- a/src/object/particlesystem_interactive.cpp +++ b/src/object/particlesystem_interactive.cpp @@ -69,9 +69,9 @@ ParticleSystem_Interactive::draw(DrawingContext& context) if (it == batches.end()) { const auto& batch_it = batches.emplace(particle->texture, SurfaceBatch(particle->texture)); - batch_it.first->second.draw(particle->pos); + batch_it.first->second.draw(particle->pos, particle->angle); } else { - it->second.draw(particle->pos); + it->second.draw(particle->pos, particle->angle); } } @@ -80,7 +80,7 @@ ParticleSystem_Interactive::draw(DrawingContext& context) auto& batch = it.second; // FIXME: What is the colour used for? context.color().draw_surface_batch(surface, batch.move_srcrects(), - batch.move_dstrects(), Color::WHITE, z_pos); + batch.move_dstrects(), batch.move_angles(), Color::WHITE, z_pos); } context.pop_transform(); diff --git a/src/object/rain_particle_system.cpp b/src/object/rain_particle_system.cpp index 3a1ed7dbe14..ee4c54949c8 100644 --- a/src/object/rain_particle_system.cpp +++ b/src/object/rain_particle_system.cpp @@ -1,5 +1,5 @@ // SuperTux -// Copyright (C) 2006 Matthias Braun +// Copyright (C) 2020 A. Semphris // // 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 @@ -17,23 +17,63 @@ #include "object/rain_particle_system.hpp" #include +#include +#include "math/easing.hpp" #include "math/random.hpp" #include "object/camera.hpp" #include "object/rainsplash.hpp" #include "supertux/sector.hpp" +#include "util/reader.hpp" +#include "util/reader_mapping.hpp" +#include "video/drawing_context.hpp" #include "video/surface.hpp" #include "video/video_system.hpp" #include "video/viewport.hpp" -RainParticleSystem::RainParticleSystem() +RainParticleSystem::RainParticleSystem() : + ExposedObject(this), + m_current_speed(1.f), + m_target_speed(1.f), + m_speed_fade_time_remaining(0.f), + + m_begin_angle(45.f), + m_current_angle(45.f), + m_target_angle(45.f), + m_angle_fade_time_remaining(0.f), + m_angle_fade_time_total(0.f), + m_angle_easing(getEasingByName(EaseNone)), + + m_current_amount(1.f), + m_target_amount(1.f), + m_amount_fade_time_remaining(0.f), + m_current_real_amount(0.f) { init(); } RainParticleSystem::RainParticleSystem(const ReaderMapping& reader) : - ParticleSystem_Interactive(reader) + ParticleSystem_Interactive(reader), + ExposedObject(this), + m_current_speed(1.f), + m_target_speed(1.f), + m_speed_fade_time_remaining(0.f), + + m_begin_angle(45.f), + m_current_angle(45.f), + m_target_angle(45.f), + m_angle_fade_time_remaining(0.f), + m_angle_fade_time_total(0.f), + m_angle_easing(getEasingByName(EaseNone)), + + m_current_amount(1.f), + m_target_amount(1.f), + m_amount_fade_time_remaining(0.f), + m_current_real_amount(0.f) { + reader.get("intensity", m_current_amount, 1.f); + reader.get("angle", m_current_angle, 1.f); + reader.get("speed", m_current_speed, 1.f); init(); } @@ -49,19 +89,58 @@ void RainParticleSystem::init() virtual_width = static_cast(SCREEN_WIDTH) * 2.0f; // create some random raindrops - size_t raindropcount = size_t(virtual_width/6.0f); - for (size_t i=0; i(); - particle->pos.x = static_cast(graphicsRandom.rand(int(virtual_width))); - particle->pos.y = static_cast(graphicsRandom.rand(int(virtual_height))); - int rainsize = graphicsRandom.rand(2); - particle->texture = rainimages[rainsize]; - do { - particle->speed = (static_cast(rainsize) + 1.0f) * 45.0f + graphicsRandom.randf(3.6f); - } while(particle->speed < 1); - - particles.push_back(std::move(particle)); + set_amount(m_current_amount); +} + +ObjectSettings +RainParticleSystem::get_settings() +{ + ObjectSettings result = ParticleSystem::get_settings(); + + result.add_float(_("Intensity"), &m_current_amount, "intensity", 1.f); + result.add_float(_("Angle"), &m_current_angle, "angle", 1.f); + result.add_float(_("Speed"), &m_current_speed, "speed", 1.f); + + result.reorder({"intensity", "angle", "speed", "enabled", "name"}); + + return result; +} + +void RainParticleSystem::set_amount(float amount) +{ + // Don't spawn too many particles to avoid destroying the player's computer + float real_amount = (amount < min_amount) ? min_amount : amount; + real_amount = (amount > max_amount) ? max_amount : amount; + + int old_raindropcount = static_cast(virtual_width*m_current_real_amount/6.0f); + int new_raindropcount = static_cast(virtual_width*real_amount/6.0f); + int delta = new_raindropcount - old_raindropcount; + + if (delta > 0) { + for (int i=0; i(); + particle->pos.x = static_cast(graphicsRandom.rand(int(virtual_width))); + particle->pos.y = static_cast(graphicsRandom.rand(int(virtual_height))); + int rainsize = graphicsRandom.rand(2); + particle->texture = rainimages[rainsize]; + do { + particle->speed = ((static_cast(rainsize) + 1.0f) * 45.0f + graphicsRandom.randf(3.6f)); + } while(particle->speed < 1); + particles.push_back(std::move(particle)); + } + } else if (delta < 0) { + for (int i=0; i>delta; --i) { + particles.pop_back(); + } } + + m_current_real_amount = real_amount; +} + +void RainParticleSystem::set_angle(float angle) +{ + for (auto& particle : particles) + particle->angle = angle; } void RainParticleSystem::update(float dt_sec) @@ -69,15 +148,63 @@ void RainParticleSystem::update(float dt_sec) if (!enabled) return; + // Update amount + if (m_amount_fade_time_remaining > 0.f) { + if (dt_sec >= m_amount_fade_time_remaining) { + m_current_amount = m_target_amount; + m_amount_fade_time_remaining = 0.f; + // Test below + /*if (m_current_amount > 1.1f) { + m_target_amount = 0.1f; + } else { + m_target_amount = 5.f; + } + m_amount_fade_time_remaining = 2.f;*/ + // Test above + } else { + float amount = dt_sec / m_amount_fade_time_remaining; + m_current_amount += (m_target_amount - m_current_amount) * amount; + m_amount_fade_time_remaining -= dt_sec; + } + } + + set_amount(m_current_amount); + + // Update speed + if (m_speed_fade_time_remaining > 0.f) { + if (dt_sec >= m_speed_fade_time_remaining) { + m_current_speed = m_target_speed; + m_speed_fade_time_remaining = 0.f; + } else { + float amount = dt_sec / m_speed_fade_time_remaining; + m_current_speed += (m_target_speed - m_current_speed) * amount; + m_speed_fade_time_remaining -= dt_sec; + } + } + + // Update angle + if (m_angle_fade_time_remaining > 0.f) { + if (dt_sec >= m_angle_fade_time_remaining) { + m_current_angle = m_target_angle; + m_angle_fade_time_remaining = 0.f; + } else { + m_angle_fade_time_remaining -= dt_sec; + float progress = 1.f - m_angle_fade_time_remaining / m_angle_fade_time_total; + progress = static_cast(m_angle_easing(progress)); + m_current_angle = progress * (m_target_angle - m_begin_angle) + m_begin_angle; + } + set_angle(m_current_angle); + } + for (auto& it : particles) { auto particle = dynamic_cast(it.get()); assert(particle); - float movement = particle->speed * dt_sec * Sector::get().get_gravity(); + float movement = particle->speed * dt_sec * Sector::get().get_gravity() * m_current_speed * 1.41421353f; float abs_x = Sector::get().get_camera().get_translation().x; float abs_y = Sector::get().get_camera().get_translation().y; - particle->pos.y += movement; - particle->pos.x -= movement; + particle->pos.y += movement * cos((particle->angle + 45.f) * 3.14159265f / 180.f); + particle->pos.x -= movement * sin((particle->angle + 45.f) * 3.14159265f / 180.f); int col = collision(particle, Vector(-movement, movement)); if ((particle->pos.y > static_cast(SCREEN_HEIGHT) + abs_y) || (col >= 0)) { //Create rainsplash @@ -107,4 +234,73 @@ void RainParticleSystem::update(float dt_sec) } } +void RainParticleSystem::fade_speed(float new_speed, float fade_time) +{ + // No check to enabled; change the fading even if it's disabled + + // If time is 0 (or smaller?), then update() will never change m_current_speed + if (fade_time <= 0.f) + { + m_current_speed = new_speed; + } + + m_target_speed = new_speed; + m_speed_fade_time_remaining = fade_time; +} + +void RainParticleSystem::fade_angle(float new_angle, float fade_time, easing ease_func) +{ + // No check to enabled; change the fading even if it's disabled + + // If time is 0 (or smaller?), then update() will never change m_current_amount + if (fade_time <= 0.f) + { + m_current_angle = new_angle - 45.f; + } + + m_begin_angle = m_current_angle; + m_target_angle = new_angle - 45.f; + m_angle_fade_time_total = fade_time; + m_angle_fade_time_remaining = fade_time; + m_angle_easing = ease_func; +} + +void RainParticleSystem::fade_amount(float new_amount, float fade_time) +{ + // No check to enabled; change the fading even if it's disabled + + // If time is 0 (or smaller?), then update() will never change m_current_amount + if (fade_time <= 0.f) + { + m_current_amount = new_amount; + } + + m_target_amount = new_amount; + m_amount_fade_time_remaining = fade_time; +} + +void RainParticleSystem::draw(DrawingContext& context) +{ + ParticleSystem_Interactive::draw(context); + + if (!enabled) + return; + + float opacity = fog_max_value * (m_current_amount - fog_start_amount) / (max_amount - fog_start_amount); + if (opacity < 0.f) + opacity = 0.f; + if (opacity > 1.f) + opacity = 1.f; + + context.push_transform(); + context.set_translation(Vector(0, 0)); + context.color().draw_filled_rect(Rectf(0, + 0, + static_cast(context.get_width()), + static_cast(context.get_height())), + Color(0.3f, 0.38f, 0.4f, opacity), + 199); // TODO: Change the hardcoded layer value with the rain's layer + context.pop_transform(); +} + /* EOF */ diff --git a/src/object/rain_particle_system.hpp b/src/object/rain_particle_system.hpp index c35e515f8d9..f90de746eb8 100644 --- a/src/object/rain_particle_system.hpp +++ b/src/object/rain_particle_system.hpp @@ -1,5 +1,5 @@ // SuperTux -// Copyright (C) 2006 Matthias Braun +// Copyright (C) 2020 A. Semphris // // 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 @@ -17,26 +17,51 @@ #ifndef HEADER_SUPERTUX_OBJECT_RAIN_PARTICLE_SYSTEM_HPP #define HEADER_SUPERTUX_OBJECT_RAIN_PARTICLE_SYSTEM_HPP +#include "math/easing.hpp" #include "object/particlesystem_interactive.hpp" +#include "scripting/rain.hpp" #include "video/surface_ptr.hpp" -class RainParticleSystem final : public ParticleSystem_Interactive +class RainParticleSystem final : + public ParticleSystem_Interactive, + public ExposedObject { public: RainParticleSystem(); RainParticleSystem(const ReaderMapping& reader); virtual ~RainParticleSystem(); + virtual void draw(DrawingContext& context) override; + void init(); virtual void update(float dt_sec) override; virtual std::string get_class() const override { return "particles-rain"; } virtual std::string get_display_name() const override { return _("Rain Particles"); } + virtual ObjectSettings get_settings() override; + + void fade_speed(float new_speed, float fade_time); + void fade_angle(float new_angle, float fade_time, easing ease_func); + void fade_amount(float new_amount, float fade_time); virtual const std::string get_icon_path() const override { return "images/engine/editor/rain.png"; } + // Minimum and maximum multiplier for the amount of particles (intensity) + static float constexpr const max_amount = 5.0f; + static float constexpr const min_amount = 0.1f; + + // Minimum value of m_current_amount for the fog to be > 0 + static float constexpr const fog_start_amount = 1.0f; + + // When m_current_amount == max_amount, fog is this value + static float constexpr const fog_max_value = 0.6f; + +private: + void set_amount(float amount); + void set_angle(float angle); + private: class RainParticle : public Particle { @@ -50,6 +75,23 @@ class RainParticleSystem final : public ParticleSystem_Interactive SurfacePtr rainimages[2]; + float m_current_speed; + float m_target_speed; + float m_speed_fade_time_remaining; + + float m_begin_angle; + float m_current_angle; + float m_target_angle; + float m_angle_fade_time_remaining; + float m_angle_fade_time_total; + easing m_angle_easing; + + float m_current_amount; + float m_target_amount; + float m_amount_fade_time_remaining; + + float m_current_real_amount; + private: RainParticleSystem(const RainParticleSystem&) = delete; RainParticleSystem& operator=(const RainParticleSystem&) = delete; diff --git a/src/scripting/clouds.cpp b/src/scripting/clouds.cpp new file mode 100644 index 00000000000..167c7e32b7f --- /dev/null +++ b/src/scripting/clouds.cpp @@ -0,0 +1,54 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "object/cloud_particle_system.hpp" +#include "scripting/clouds.hpp" + +namespace scripting { + +void Clouds::set_enabled(bool enable) +{ + SCRIPT_GUARD_VOID; + object.set_enabled(enable); +} + +bool Clouds::get_enabled() const +{ + SCRIPT_GUARD_DEFAULT; + return object.get_enabled(); +} + +void Clouds::fade_speed(float speed, float time) +{ + SCRIPT_GUARD_VOID; + object.fade_speed(speed, time); +} + +void Clouds::fade_amount(int amount, float time, float time_between) +{ + SCRIPT_GUARD_VOID; + object.fade_amount(amount, time, time_between); +} + +void Clouds::set_amount(int amount, float time) +{ + SCRIPT_GUARD_VOID; + object.fade_amount(amount, time); +} + +} // namespace scripting + +/* EOF */ diff --git a/src/scripting/clouds.hpp b/src/scripting/clouds.hpp new file mode 100644 index 00000000000..a21ef8faf20 --- /dev/null +++ b/src/scripting/clouds.hpp @@ -0,0 +1,60 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_SCRIPTING_CLOUDS_HPP +#define HEADER_SUPERTUX_SCRIPTING_CLOUDS_HPP + +#ifndef SCRIPTING_API +#include "scripting/game_object.hpp" + +class CloudParticleSystem; +#endif + +namespace scripting { + +class Clouds final +#ifndef SCRIPTING_API + : public GameObject<::CloudParticleSystem> +#endif +{ +#ifndef SCRIPTING_API +public: + using GameObject::GameObject; + +private: + Clouds(const Clouds&) = delete; + Clouds& operator=(const Clouds&) = delete; +#endif + +public: + void set_enabled(bool enable); + bool get_enabled() const; + + /** Smoothly changes the rain speed to the given value */ + void fade_speed(float speed, float time); + + /** Smoothly changes the amount of particles to the given value */ + void fade_amount(int amount, float time, float time_between); + + /** Smoothly changes the amount of particles to the given value */ + void set_amount(int amount, float time); +}; + +} // namespace scripting + +#endif + +/* EOF */ diff --git a/src/scripting/rain.cpp b/src/scripting/rain.cpp new file mode 100644 index 00000000000..e55e0a4a7ee --- /dev/null +++ b/src/scripting/rain.cpp @@ -0,0 +1,55 @@ +// SuperTux +// Copyright (C) 2006 Matthias Braun +// +// 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 "math/easing.hpp" +#include "object/rain_particle_system.hpp" +#include "scripting/rain.hpp" + +namespace scripting { + +void Rain::set_enabled(bool enable) +{ + SCRIPT_GUARD_VOID; + object.set_enabled(enable); +} + +bool Rain::get_enabled() const +{ + SCRIPT_GUARD_DEFAULT; + return object.get_enabled(); +} + +void Rain::fade_speed(float speed, float time) +{ + SCRIPT_GUARD_VOID; + object.fade_speed(speed, time); +} + +void Rain::fade_amount(float amount, float time) +{ + SCRIPT_GUARD_VOID; + object.fade_amount(amount, time); +} + +void Rain::fade_angle(float angle, float time, std::string ease) +{ + SCRIPT_GUARD_VOID; + object.fade_angle(angle, time, getEasingByName(EasingMode_from_string(ease))); +} + +} // namespace scripting + +/* EOF */ diff --git a/src/scripting/rain.hpp b/src/scripting/rain.hpp new file mode 100644 index 00000000000..e494193b270 --- /dev/null +++ b/src/scripting/rain.hpp @@ -0,0 +1,60 @@ +// SuperTux +// Copyright (C) 2006 Matthias Braun +// +// 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 . + +#ifndef HEADER_SUPERTUX_SCRIPTING_RAIN_HPP +#define HEADER_SUPERTUX_SCRIPTING_RAIN_HPP + +#ifndef SCRIPTING_API +#include "scripting/game_object.hpp" + +class RainParticleSystem; +#endif + +namespace scripting { + +class Rain final +#ifndef SCRIPTING_API + : public GameObject<::RainParticleSystem> +#endif +{ +#ifndef SCRIPTING_API +public: + using GameObject::GameObject; + +private: + Rain(const Rain&) = delete; + Rain& operator=(const Rain&) = delete; +#endif + +public: + void set_enabled(bool enable); + bool get_enabled() const; + + /** Smoothly changes the rain speed to the given value */ + void fade_speed(float speed, float time); + + /** Smoothly changes the amount of particles to the given value */ + void fade_amount(float amount, float time); + + /** Smoothly changes the angle of the rain according to the easing function */ + void fade_angle(float angle, float time, std::string ease); +}; + +} // namespace scripting + +#endif + +/* EOF */ diff --git a/src/scripting/wrapper.cpp b/src/scripting/wrapper.cpp index 976fcee8647..7888f96121f 100644 --- a/src/scripting/wrapper.cpp +++ b/src/scripting/wrapper.cpp @@ -647,6 +647,199 @@ static SQInteger Candle_set_burning_wrapper(HSQUIRRELVM vm) } +static SQInteger Clouds_release_hook(SQUserPointer ptr, SQInteger ) +{ + auto _this = reinterpret_cast (ptr); + delete _this; + return 0; +} + +static SQInteger Clouds_set_enabled_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'set_enabled' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQBool arg0; + if(SQ_FAILED(sq_getbool(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a bool")); + return SQ_ERROR; + } + + try { + _this->set_enabled(arg0 == SQTrue); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'set_enabled'")); + return SQ_ERROR; + } + +} + +static SQInteger Clouds_get_enabled_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'get_enabled' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + + try { + bool return_value = _this->get_enabled(); + + sq_pushbool(vm, return_value); + return 1; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'get_enabled'")); + return SQ_ERROR; + } + +} + +static SQInteger Clouds_fade_speed_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'fade_speed' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQFloat arg0; + if(SQ_FAILED(sq_getfloat(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a float")); + return SQ_ERROR; + } + SQFloat arg1; + if(SQ_FAILED(sq_getfloat(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a float")); + return SQ_ERROR; + } + + try { + _this->fade_speed(static_cast (arg0), static_cast (arg1)); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'fade_speed'")); + return SQ_ERROR; + } + +} + +static SQInteger Clouds_fade_amount_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'fade_amount' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQInteger arg0; + if(SQ_FAILED(sq_getinteger(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not an integer")); + return SQ_ERROR; + } + SQFloat arg1; + if(SQ_FAILED(sq_getfloat(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a float")); + return SQ_ERROR; + } + SQFloat arg2; + if(SQ_FAILED(sq_getfloat(vm, 4, &arg2))) { + sq_throwerror(vm, _SC("Argument 3 not a float")); + return SQ_ERROR; + } + + try { + _this->fade_amount(static_cast (arg0), static_cast (arg1), static_cast (arg2)); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'fade_amount'")); + return SQ_ERROR; + } + +} + +static SQInteger Clouds_set_amount_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'set_amount' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQInteger arg0; + if(SQ_FAILED(sq_getinteger(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not an integer")); + return SQ_ERROR; + } + SQFloat arg1; + if(SQ_FAILED(sq_getfloat(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a float")); + return SQ_ERROR; + } + + try { + _this->set_amount(static_cast (arg0), static_cast (arg1)); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'set_amount'")); + return SQ_ERROR; + } + +} + static SQInteger Decal_release_hook(SQUserPointer ptr, SQInteger ) { auto _this = reinterpret_cast (ptr); @@ -3113,6 +3306,199 @@ static SQInteger Player_get_velocity_y_wrapper(HSQUIRRELVM vm) } +static SQInteger Rain_release_hook(SQUserPointer ptr, SQInteger ) +{ + auto _this = reinterpret_cast (ptr); + delete _this; + return 0; +} + +static SQInteger Rain_set_enabled_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'set_enabled' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQBool arg0; + if(SQ_FAILED(sq_getbool(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a bool")); + return SQ_ERROR; + } + + try { + _this->set_enabled(arg0 == SQTrue); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'set_enabled'")); + return SQ_ERROR; + } + +} + +static SQInteger Rain_get_enabled_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'get_enabled' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + + try { + bool return_value = _this->get_enabled(); + + sq_pushbool(vm, return_value); + return 1; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'get_enabled'")); + return SQ_ERROR; + } + +} + +static SQInteger Rain_fade_speed_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'fade_speed' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQFloat arg0; + if(SQ_FAILED(sq_getfloat(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a float")); + return SQ_ERROR; + } + SQFloat arg1; + if(SQ_FAILED(sq_getfloat(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a float")); + return SQ_ERROR; + } + + try { + _this->fade_speed(static_cast (arg0), static_cast (arg1)); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'fade_speed'")); + return SQ_ERROR; + } + +} + +static SQInteger Rain_fade_amount_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'fade_amount' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQFloat arg0; + if(SQ_FAILED(sq_getfloat(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a float")); + return SQ_ERROR; + } + SQFloat arg1; + if(SQ_FAILED(sq_getfloat(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a float")); + return SQ_ERROR; + } + + try { + _this->fade_amount(static_cast (arg0), static_cast (arg1)); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'fade_amount'")); + return SQ_ERROR; + } + +} + +static SQInteger Rain_fade_angle_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'fade_angle' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQFloat arg0; + if(SQ_FAILED(sq_getfloat(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a float")); + return SQ_ERROR; + } + SQFloat arg1; + if(SQ_FAILED(sq_getfloat(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a float")); + return SQ_ERROR; + } + const SQChar* arg2; + if(SQ_FAILED(sq_getstring(vm, 4, &arg2))) { + sq_throwerror(vm, _SC("Argument 3 not a string")); + return SQ_ERROR; + } + + try { + _this->fade_angle(static_cast (arg0), static_cast (arg1), arg2); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'fade_angle'")); + return SQ_ERROR; + } + +} + static SQInteger Rock_release_hook(SQUserPointer ptr, SQInteger ) { auto _this = reinterpret_cast (ptr); @@ -7107,6 +7493,32 @@ void create_squirrel_instance(HSQUIRRELVM v, scripting::Candle* object, bool set sq_remove(v, -2); // remove root table } +void create_squirrel_instance(HSQUIRRELVM v, scripting::Clouds* object, bool setup_releasehook) +{ + using namespace wrapper; + + sq_pushroottable(v); + sq_pushstring(v, "Clouds", -1); + if(SQ_FAILED(sq_get(v, -2))) { + std::ostringstream msg; + msg << "Couldn't resolved squirrel type 'Clouds'"; + throw SquirrelError(v, msg.str()); + } + + if(SQ_FAILED(sq_createinstance(v, -1)) || SQ_FAILED(sq_setinstanceup(v, -1, object))) { + std::ostringstream msg; + msg << "Couldn't setup squirrel instance for object of type 'Clouds'"; + throw SquirrelError(v, msg.str()); + } + sq_remove(v, -2); // remove object name + + if(setup_releasehook) { + sq_setreleasehook(v, -1, Clouds_release_hook); + } + + sq_remove(v, -2); // remove root table +} + void create_squirrel_instance(HSQUIRRELVM v, scripting::Decal* object, bool setup_releasehook) { using namespace wrapper; @@ -7341,6 +7753,32 @@ void create_squirrel_instance(HSQUIRRELVM v, scripting::Player* object, bool set sq_remove(v, -2); // remove root table } +void create_squirrel_instance(HSQUIRRELVM v, scripting::Rain* object, bool setup_releasehook) +{ + using namespace wrapper; + + sq_pushroottable(v); + sq_pushstring(v, "Rain", -1); + if(SQ_FAILED(sq_get(v, -2))) { + std::ostringstream msg; + msg << "Couldn't resolved squirrel type 'Rain'"; + throw SquirrelError(v, msg.str()); + } + + if(SQ_FAILED(sq_createinstance(v, -1)) || SQ_FAILED(sq_setinstanceup(v, -1, object))) { + std::ostringstream msg; + msg << "Couldn't setup squirrel instance for object of type 'Rain'"; + throw SquirrelError(v, msg.str()); + } + sq_remove(v, -2); // remove object name + + if(setup_releasehook) { + sq_setreleasehook(v, -1, Rain_release_hook); + } + + sq_remove(v, -2); // remove root table +} + void create_squirrel_instance(HSQUIRRELVM v, scripting::Rock* object, bool setup_releasehook) { using namespace wrapper; @@ -8169,6 +8607,52 @@ void register_supertux_wrapper(HSQUIRRELVM v) throw SquirrelError(v, "Couldn't register class 'Candle'"); } + // Register class Clouds + sq_pushstring(v, "Clouds", -1); + if(sq_newclass(v, SQFalse) < 0) { + std::ostringstream msg; + msg << "Couldn't create new class 'Clouds'"; + throw SquirrelError(v, msg.str()); + } + sq_pushstring(v, "set_enabled", -1); + sq_newclosure(v, &Clouds_set_enabled_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tb"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'set_enabled'"); + } + + sq_pushstring(v, "get_enabled", -1); + sq_newclosure(v, &Clouds_get_enabled_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|t"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'get_enabled'"); + } + + sq_pushstring(v, "fade_speed", -1); + sq_newclosure(v, &Clouds_fade_speed_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tnn"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'fade_speed'"); + } + + sq_pushstring(v, "fade_amount", -1); + sq_newclosure(v, &Clouds_fade_amount_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tinn"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'fade_amount'"); + } + + sq_pushstring(v, "set_amount", -1); + sq_newclosure(v, &Clouds_set_amount_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tin"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'set_amount'"); + } + + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register class 'Clouds'"); + } + // Register class Decal sq_pushstring(v, "Decal", -1); if(sq_newclass(v, SQFalse) < 0) { @@ -8765,6 +9249,52 @@ void register_supertux_wrapper(HSQUIRRELVM v) throw SquirrelError(v, "Couldn't register class 'Player'"); } + // Register class Rain + sq_pushstring(v, "Rain", -1); + if(sq_newclass(v, SQFalse) < 0) { + std::ostringstream msg; + msg << "Couldn't create new class 'Rain'"; + throw SquirrelError(v, msg.str()); + } + sq_pushstring(v, "set_enabled", -1); + sq_newclosure(v, &Rain_set_enabled_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tb"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'set_enabled'"); + } + + sq_pushstring(v, "get_enabled", -1); + sq_newclosure(v, &Rain_get_enabled_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|t"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'get_enabled'"); + } + + sq_pushstring(v, "fade_speed", -1); + sq_newclosure(v, &Rain_fade_speed_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tnn"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'fade_speed'"); + } + + sq_pushstring(v, "fade_amount", -1); + sq_newclosure(v, &Rain_fade_amount_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tnn"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'fade_amount'"); + } + + sq_pushstring(v, "fade_angle", -1); + sq_newclosure(v, &Rain_fade_angle_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tnns"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'fade_angle'"); + } + + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register class 'Rain'"); + } + // Register class Rock sq_pushstring(v, "Rock", -1); if(sq_newclass(v, SQFalse) < 0) { diff --git a/src/scripting/wrapper.hpp b/src/scripting/wrapper.hpp index 22df8f40d84..0d42ae3c24a 100644 --- a/src/scripting/wrapper.hpp +++ b/src/scripting/wrapper.hpp @@ -22,6 +22,8 @@ class Camera; void create_squirrel_instance(HSQUIRRELVM v, scripting::Camera* object, bool setup_releasehook = false); class Candle; void create_squirrel_instance(HSQUIRRELVM v, scripting::Candle* object, bool setup_releasehook = false); +class Clouds; +void create_squirrel_instance(HSQUIRRELVM v, scripting::Clouds* object, bool setup_releasehook = false); class Decal; void create_squirrel_instance(HSQUIRRELVM v, scripting::Decal* object, bool setup_releasehook = false); class Dispenser; @@ -40,6 +42,8 @@ class Platform; void create_squirrel_instance(HSQUIRRELVM v, scripting::Platform* object, bool setup_releasehook = false); class Player; void create_squirrel_instance(HSQUIRRELVM v, scripting::Player* object, bool setup_releasehook = false); +class Rain; +void create_squirrel_instance(HSQUIRRELVM v, scripting::Rain* object, bool setup_releasehook = false); class Rock; void create_squirrel_instance(HSQUIRRELVM v, scripting::Rock* object, bool setup_releasehook = false); class ScriptedObject; diff --git a/src/scripting/wrapper.interface.hpp b/src/scripting/wrapper.interface.hpp index 47eb8c4f998..17a5c637030 100644 --- a/src/scripting/wrapper.interface.hpp +++ b/src/scripting/wrapper.interface.hpp @@ -5,6 +5,7 @@ #include "scripting/badguy.hpp" #include "scripting/camera.hpp" #include "scripting/candle.hpp" +#include "scripting/clouds.hpp" #include "scripting/decal.hpp" #include "scripting/dispenser.hpp" #include "scripting/display_effect.hpp" @@ -16,6 +17,7 @@ #include "scripting/particlesystem.hpp" #include "scripting/platform.hpp" #include "scripting/player.hpp" +#include "scripting/rain.hpp" #include "scripting/rock.hpp" #include "scripting/scripted_object.hpp" #include "scripting/sector.hpp" diff --git a/src/video/surface_batch.cpp b/src/video/surface_batch.cpp index 899eada7623..9fe6c4c6711 100644 --- a/src/video/surface_batch.cpp +++ b/src/video/surface_batch.cpp @@ -19,8 +19,9 @@ #include "math/rectf.hpp" #include "video/surface.hpp" -SurfaceBatch::SurfaceBatch(const SurfacePtr& surface) : +SurfaceBatch::SurfaceBatch(const SurfacePtr& surface, const Color& color) : m_surface(surface), + m_color(color), m_srcrects(), m_dstrects(), m_angles() diff --git a/src/video/surface_batch.hpp b/src/video/surface_batch.hpp index f39257f671f..58fb623cb4d 100644 --- a/src/video/surface_batch.hpp +++ b/src/video/surface_batch.hpp @@ -28,7 +28,7 @@ class Vector; class SurfaceBatch { public: - SurfaceBatch(const SurfacePtr& surface); + SurfaceBatch(const SurfacePtr& surface, const Color& color = Color::WHITE); SurfaceBatch(SurfaceBatch&&) = default; void draw(const Vector& pos, float angle = 0.0f); @@ -39,8 +39,11 @@ class SurfaceBatch std::vector move_dstrects() { return std::move(m_dstrects); } std::vector move_angles() { return std::move(m_angles); } + Color get_color() { return m_color; } + private: SurfacePtr m_surface; + Color m_color; std::vector m_srcrects; std::vector m_dstrects; std::vector m_angles; From 3a05806b4abc4ab4703e0837c15026fdd35681eb Mon Sep 17 00:00:00 2001 From: Semphris Date: Sun, 20 Sep 2020 17:33:04 -0400 Subject: [PATCH 2/9] Custom particles, alpha version (big commit #1) What has been done : - The custom particle object exists and in instanciable (creatable) from the editor toolbox - The particles show up and are manageable with plenty of settings already (I intend to add even more if needed) - The particles spawn and death locations can be handled via a new particle area object (light green, has the same sparkle icon as the custom particle object itself) TODO: Scripting (Every single setting should have getters, setters, faders and easers (when relevant) in Squirrel) TODO: Make a particle editor alongside the level editor, because jeez, that's waaaaay too many settings TODO: Store custom particle data in standalone file for easy editing reusability (and allow users to load custom particle data from files) ... and probably much more to do and fix :) --- data/images/engine/editor/objects.stoi | 6 + data/images/engine/editor/particle_zone.png | Bin 0 -> 4705 bytes data/images/engine/editor/sparkle.png | Bin 0 -> 4671 bytes src/badguy/badguy.cpp | 4 +- src/object/background.cpp | 2 +- src/object/cloud_particle_system.cpp | 8 +- src/object/cloud_particle_system.hpp | 4 +- src/object/custom_particle_system.cpp | 942 ++++++++++++++++++++ src/object/custom_particle_system.hpp | 235 +++++ src/object/display_effect.cpp | 38 +- src/object/display_effect.hpp | 2 +- src/object/particle_zone.cpp | 133 +++ src/object/particle_zone.hpp | 113 +++ src/object/particlesystem.hpp | 4 +- src/object/particlesystem_interactive.hpp | 2 +- src/object/path.cpp | 2 +- src/object/rain_particle_system.cpp | 2 +- src/scripting/custom_particles.cpp | 54 ++ src/scripting/custom_particles.hpp | 60 ++ src/scripting/wrapper.cpp | 265 ++++++ src/scripting/wrapper.hpp | 2 + src/scripting/wrapper.interface.hpp | 1 + src/supertux/game_object_factory.cpp | 4 + src/supertux/sector_parser.cpp | 3 + 24 files changed, 1853 insertions(+), 33 deletions(-) create mode 100644 data/images/engine/editor/particle_zone.png create mode 100644 data/images/engine/editor/sparkle.png create mode 100644 src/object/custom_particle_system.cpp create mode 100644 src/object/custom_particle_system.hpp create mode 100644 src/object/particle_zone.cpp create mode 100644 src/object/particle_zone.hpp create mode 100644 src/scripting/custom_particles.cpp create mode 100644 src/scripting/custom_particles.hpp diff --git a/data/images/engine/editor/objects.stoi b/data/images/engine/editor/objects.stoi index e8f51ebb452..20b3e4ef33a 100644 --- a/data/images/engine/editor/objects.stoi +++ b/data/images/engine/editor/objects.stoi @@ -303,6 +303,9 @@ (object (class "wind") (icon "images/engine/editor/wind.png")) + (object + (class "particle-zone") + (icon "images/engine/editor/particle_zone.png")) (object (class "skull_tile") (icon "images/objects/skull_tile/skull.png")) @@ -335,6 +338,9 @@ (object (class "particles-snow") (icon "images/engine/editor/snow.png")) + (object + (class "particles-custom") + (icon "images/engine/editor/sparkle.png")) (object (class "thunderstorm") (icon "images/engine/editor/thunderstorm.png")) diff --git a/data/images/engine/editor/particle_zone.png b/data/images/engine/editor/particle_zone.png new file mode 100644 index 0000000000000000000000000000000000000000..c924ee02a21dba65b290391d392b55b2c8f13ced GIT binary patch literal 4705 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 literal 0 HcmV?d00001 diff --git a/data/images/engine/editor/sparkle.png b/data/images/engine/editor/sparkle.png new file mode 100644 index 0000000000000000000000000000000000000000..f0cc4d0fb045c77f112ba66a0416d8826e676581 GIT binary patch literal 4671 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
    PpGe4^#RZmFcmCn>C zx`YJzj9$@q@1Yv28Y({Gqr z0g*PbnkWTPfojPM8BGl_?4RnBtMy6xrg*7zRbM-=f?1`x^=K2Bn+It2%N%TXpf(3~ zxl&S5iLnHW4oR^o_VlIfjnyvZW~00D$)LqkwWLqi~Na&Km7Y-IodD3N`UJxIeq9K~P1s7l4b4k`{A zs*?p#5l5{;5h{dQp;ZTyOTVB=Lz3d+D7Y3J{8+3yxH#+T;3^1$A0SSSPKqv4;{TFD zix>}%`|cQbEH!v;xsKZrdviQ%{%O7`R%`;h zezZ~ccn41{(_$W2dcQ$+Wm{a}+c)-6@qF*F_tHpR;BWku@Al?XzCLBdLr)!S!s#%` zD=vx({`=8JPMz{lR&jfLHQ;tYdBwdfTY83ugH61&Qu|lPOA1U*A3DPX_YjGRq3hrO zdq&l^F8-*kqr1y>L-42(0I05P!|U~p@b0X~3{G&kSDq59|$YAgaXeEpb~1cZPl zV5#sNWVszE#58@tJ%Z3apb=TJI9_{O%=U~*plp%B4BsVm!zMg6fOcRG;6k;h0TJ}j z6TR;H@v`|z)avizyl=WPVK~%9K@A8fIC$ab>aaWfJr!P zdCS5l98nk>xB@`w+4c5#CzJN(8$zOjx%tO%q7OLc{Cz z(^Bi_@R1zqPrBH=!x2?Y0!u`NtGH8xs5TR@2pe_+J;^{2=m5?F7l05p zHAvMi7q4z|k6wtb*~Me^NU}$OOTfdxMC{H%AcP*800e*rpdaYE`fO>OsGr2LH5p@X zLjZ?EM>o2G0pJzU2?}AoOMqnnjUq*jz$L&!l86c(7l997?p}o5O5ht1Nwj+z)GNs?Cik!bYO12 zNmgdJ$a@>mh9XC>soh8zK(;8je!wrHWC33S8B9#;qim5eb^`Ce*NSe~P?a#c7NEH) zolif@XYumc`2ESi&thc+i2)LvgPcE?di7xG5=L0%<+UxNC*XF#3oA78rd`I+RX+Zt zfV|=&7Ol!;<8~Ky=Vdl;os8Yq2Sl)`VScH}CcR)T53IUT2Lx^~A|C#>g}ZV)*!7inSf5|pQ?;#&g$vHJ>gm}W zs8ix2Q1in@wtkewH$T~;hHuM;esXh%Sn_Q2YJan0km7m0Jo02_Yy$7B8{myCNpVfO zbMj08j-8cbj_*q!GMJjH^VT-k=#}u+ui5{az+crfo={Gjp^yLo002ovPDHLkV1mLI B**5?H literal 0 HcmV?d00001 diff --git a/src/badguy/badguy.cpp b/src/badguy/badguy.cpp index ca20e4b1bc0..0e71922760c 100644 --- a/src/badguy/badguy.cpp +++ b/src/badguy/badguy.cpp @@ -168,7 +168,7 @@ BadGuy::update(float dt_sec) badguy = badguy.substr(path_chars + 1, badguy.length() - path_chars); // log warning since badguys_killed can no longer reach total_badguys std::string current_level = "[" + Sector::get().get_level()->filename + "] "; - log_warning << current_level << "Counted badguy " << badguy << " starting at " << start_position << " has left the sector" < +// +// 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 "object/custom_particle_system.hpp" + +#include +#include + +#include "collision/collision.hpp" +#include "math/aatriangle.hpp" +#include "math/easing.hpp" +#include "math/random.hpp" +#include "object/camera.hpp" +#include "object/particle_zone.hpp" +#include "object/tilemap.hpp" +#include "supertux/game_session.hpp" +#include "supertux/sector.hpp" +#include "supertux/tile.hpp" +#include "util/reader.hpp" +#include "util/reader_mapping.hpp" +#include "video/drawing_context.hpp" +#include "video/surface.hpp" +#include "video/surface_batch.hpp" +#include "video/video_system.hpp" +#include "video/viewport.hpp" + +#define PI 3.1415926535897f + +CustomParticleSystem::CustomParticleSystem() : + ExposedObject(this), + texture_sum_odds(0.f), + time_last_remaining(0.f), + m_textures(), + custom_particles(), + m_particle_main_texture("/images/engine/editor/sparkle.png"), + m_max_amount(25), + m_delay(0.1f), + m_particle_lifetime(5.f), + m_particle_lifetime_variation(0.f), + m_particle_birth_time(0.f), + m_particle_birth_time_variation(0.f), + m_particle_death_time(0.f), + m_particle_death_time_variation(0.f), + m_particle_birth_mode(), + m_particle_death_mode(), + m_particle_birth_easing(), + m_particle_death_easing(), + m_particle_speed_x(0.f), + m_particle_speed_y(0.f), + m_particle_speed_variation_x(0.f), + m_particle_speed_variation_y(0.f), + m_particle_acceleration_x(0.f), + m_particle_acceleration_y(0.f), + m_particle_friction_x(0.f), + m_particle_friction_y(0.f), + m_particle_feather_factor(0.f), + m_particle_rotation(0.f), + m_particle_rotation_variation(0.f), + m_particle_rotation_speed(0.f), + m_particle_rotation_speed_variation(0.f), + m_particle_rotation_acceleration(0.f), + m_particle_rotation_decceleration(0.f), + m_particle_rotation_mode(), + m_particle_collision_mode(), + m_particle_offscreen_mode(), + m_cover_screen(true) +{ + init(); +} + +CustomParticleSystem::CustomParticleSystem(const ReaderMapping& reader) : + ParticleSystem_Interactive(reader), + ExposedObject(this), + texture_sum_odds(0.f), + time_last_remaining(0.f), + m_textures(), + custom_particles(), + m_particle_main_texture("/images/engine/editor/sparkle.png"), + m_max_amount(25), + m_delay(0.1f), + m_particle_lifetime(5.f), + m_particle_lifetime_variation(0.f), + m_particle_birth_time(0.f), + m_particle_birth_time_variation(0.f), + m_particle_death_time(0.f), + m_particle_death_time_variation(0.f), + m_particle_birth_mode(), + m_particle_death_mode(), + m_particle_birth_easing(), + m_particle_death_easing(), + m_particle_speed_x(0.f), + m_particle_speed_y(0.f), + m_particle_speed_variation_x(0.f), + m_particle_speed_variation_y(0.f), + m_particle_acceleration_x(0.f), + m_particle_acceleration_y(0.f), + m_particle_friction_x(0.f), + m_particle_friction_y(0.f), + m_particle_feather_factor(0.f), + m_particle_rotation(0.f), + m_particle_rotation_variation(0.f), + m_particle_rotation_speed(0.f), + m_particle_rotation_speed_variation(0.f), + m_particle_rotation_acceleration(0.f), + m_particle_rotation_decceleration(0.f), + m_particle_rotation_mode(), + m_particle_collision_mode(), + m_particle_offscreen_mode(), + m_cover_screen(true) +{ + reader.get("main-texture", m_particle_main_texture, "/images/engine/editor/sparkle.png"); + + reader.get("amount", m_max_amount, 25); + reader.get("delay", m_delay, 0.1f); + reader.get("lifetime", m_particle_lifetime, 5.f); + reader.get("lifetime-variation", m_particle_lifetime_variation, 0.f); + reader.get("birth-time", m_particle_birth_time, 0.f); + reader.get("birth-time-variation", m_particle_birth_time_variation, 0.f); + reader.get("death-time", m_particle_death_time, 0.f); + reader.get("death-time-variation", m_particle_death_time_variation, 0.f); + + reader.get("speed-x", m_particle_speed_x, 0.f); + reader.get("speed-y", m_particle_speed_y, 0.f); + reader.get("speed-var-x", m_particle_speed_variation_x, 0.f); + reader.get("speed-var-y", m_particle_speed_variation_y, 0.f); + reader.get("acceleration-x", m_particle_acceleration_x, 0.f); + reader.get("acceleration-y", m_particle_acceleration_y, 0.f); + reader.get("friction-x", m_particle_friction_x, 0.f); + reader.get("friction-y", m_particle_friction_y, 0.f); + reader.get("feather-factor", m_particle_feather_factor, 0.f); + + reader.get("rotation", m_particle_rotation, 0.f); + reader.get("rotation-variation", m_particle_rotation_variation, 0.f); + reader.get("rotation-speed", m_particle_rotation_speed, 0.f); + reader.get("rotation-speed-variation", m_particle_rotation_speed_variation, 0.f); + reader.get("rotation-acceleration", m_particle_rotation_acceleration, 0.f); + reader.get("rotation-decceleration", m_particle_rotation_decceleration, 0.f); + + reader.get("cover-screen", m_cover_screen, true); + + std::string rotation_mode; + if (reader.get("rotation-mode", rotation_mode)) + { + if (rotation_mode == "wiggling") + { + m_particle_rotation_mode = RotationMode::Wiggling; + } + else if (rotation_mode == "facing") + { + m_particle_rotation_mode = RotationMode::Facing; + } + else + { + m_particle_rotation_mode = RotationMode::Fixed; + } + } + else + { + m_particle_rotation_mode = RotationMode::Fixed; + } + + std::string birth_mode; + if (reader.get("birth-mode", birth_mode)) + { + if (birth_mode == "shrink") + { + m_particle_birth_mode = FadeMode::Shrink; + } + else if (birth_mode == "fade") + { + m_particle_birth_mode = FadeMode::Fade; + } + else + { + m_particle_birth_mode = FadeMode::None; + } + } + else + { + m_particle_birth_mode = FadeMode::None; + } + + std::string death_mode; + if (reader.get("death-mode", death_mode)) + { + if (death_mode == "shrink") + { + m_particle_death_mode = FadeMode::Shrink; + } + else if (death_mode == "fade") + { + m_particle_death_mode = FadeMode::Fade; + } + else + { + m_particle_death_mode = FadeMode::None; + } + } + else + { + m_particle_death_mode = FadeMode::None; + } + + std::string birth_easing; + if (reader.get("birth-easing", birth_easing)) + { + m_particle_birth_easing = EasingMode_from_string(birth_easing); + } + else + { + m_particle_birth_easing = EasingMode_from_string(""); + } + + std::string death_easing; + if (reader.get("death-easing", death_easing)) + { + m_particle_death_easing = EasingMode_from_string(death_easing); + } + else + { + m_particle_death_easing = EasingMode_from_string(""); + } + + std::string collision_mode; + if (reader.get("collision-mode", collision_mode)) + { + if (collision_mode == "stick") + { + m_particle_collision_mode = CollisionMode::Stick; + } + else if (collision_mode == "bounce-heavy") + { + m_particle_collision_mode = CollisionMode::BounceHeavy; + } + else if (collision_mode == "bounce-light") + { + m_particle_collision_mode = CollisionMode::BounceLight; + } + else if (collision_mode == "destroy") + { + m_particle_collision_mode = CollisionMode::Destroy; + } + else + { + m_particle_collision_mode = CollisionMode::Ignore; + } + } + else + { + m_particle_collision_mode = CollisionMode::Ignore; + } + + std::string offscreen_mode; + if (reader.get("offscreen-mode", offscreen_mode)) + { + if (offscreen_mode == "always") + { + m_particle_offscreen_mode = OffscreenMode::Always; + } + else if (offscreen_mode == "only-on-exit") + { + m_particle_offscreen_mode = OffscreenMode::OnlyOnExit; + } + else + { + m_particle_offscreen_mode = OffscreenMode::Never; + } + } + else + { + m_particle_offscreen_mode = OffscreenMode::Never; + } + + init(); +} + +CustomParticleSystem::~CustomParticleSystem() +{ +} + +void +CustomParticleSystem::init() +{ + // TODO: Multiple textures for a single particle system object? + // TODO: Handle color and scale multipliers per-texture + + //m_textures.push_back(SpriteProperties(Surface::from_file("images/engine/editor/sparkle.png"))); + //for (float r = 0.f; r < 1.f; r += 0.5f) { + // for (float g = 0.f; g < 1.f; g += 0.5f) { + // for (float b = 0.f; b < 1.f; b += 0.5f) { + auto props = SpriteProperties(Surface::from_file(m_particle_main_texture)); + props.likeliness = 1.f; + props.color = Color(1.f, 1.f, 1.f, 1.f); + props.scale = Vector(1.f, 1.f); + m_textures.push_back(props); + // } + // } + //} + texture_sum_odds = 1.f; +} + +ObjectSettings +CustomParticleSystem::get_settings() +{ + ObjectSettings result = ParticleSystem::get_settings(); + + result.add_surface(_("Texture"), &m_particle_main_texture, "main-texture"); + result.add_int(_("Amount"), &m_max_amount, "amount", 25); + result.add_float(_("Delay"), &m_delay, "delay", 0.1f); + result.add_float(_("Lifetime"), &m_particle_lifetime, "lifetime", 5.f); + result.add_float(_("Lifetime variation"), &m_particle_lifetime_variation, "lifetime-variation", 0.f); + result.add_enum(_("Birth mode"), reinterpret_cast(&m_particle_birth_mode), + {_("None"), _("Fade"), _("Shrink")}, + {"none", "fade", "shrink"}, + static_cast(FadeMode::None), + "birth-mode"); + result.add_enum(_("Birth easing"), reinterpret_cast(&m_particle_birth_easing), + { + _("No easing"), + _("Quad in"), _("Quad out"), _("Quad in/out"), + _("Cubic in"), _("Cubic out"), _("Cubic in/out"), + _("Quart in"), _("Quart out"), _("Quart in/out"), + _("Quint in"), _("Quint out"), _("Quint in/out"), + _("Sine in"), _("Sine out"), _("Sine in/out"), + _("Circular in"), _("Circular out"), _("Circular in/out"), + _("Exponential in"), _("Exponential out"), _("Exponential in/out"), + _("Elastic in"), _("Elastic out"), _("Elastic in/out"), + _("Back in"), _("Back out"), _("Back in/out"), + _("Bounce in"), _("Bounce out"), _("Bounce in/out") + }, + { + "EaseNone", + "EaseQuadIn", "EaseQuadOut", "EaseQuadInOut", + "EaseCubicIn", "EaseCubicOut", "EaseCubicInOut", + "EaseQuartIn", "EaseQuartOut", "EaseQuartInOut", + "EaseQuintIn", "EaseQuintOut", "EaseQuintInOut", + "EaseSineIn", "EaseSineOut", "EaseSineInOut", + "EaseCircularIn", "EaseCircularOut", "EaseCircularInOut", + "EaseExponentialIn", "EaseExponentialOut", "EaseExponentialInOut", + "EaseElasticIn", "EaseElasticOut", "EaseElasticInOut", + "EaseBackIn", "EaseBackOut", "EaseBackInOut", + "EaseBounceIn", "EaseBounceOut", "EaseBounceInOut" + }, + 0, "birth-easing"); + result.add_float(_("Birth time"), &m_particle_birth_time, "birth-time", 5.f); + result.add_float(_("Birth time variation"), &m_particle_birth_time_variation, "birth-time-variation", 0.f); + result.add_enum(_("Death mode"), reinterpret_cast(&m_particle_death_mode), + {_("None"), _("Fade"), _("Shrink")}, + {"none", "fade", "shrink"}, + static_cast(FadeMode::None), + "death-mode"); + result.add_enum(_("Death easing"), reinterpret_cast(&m_particle_death_easing), + { + _("No easing"), + _("Quad in"), _("Quad out"), _("Quad in/out"), + _("Cubic in"), _("Cubic out"), _("Cubic in/out"), + _("Quart in"), _("Quart out"), _("Quart in/out"), + _("Quint in"), _("Quint out"), _("Quint in/out"), + _("Sine in"), _("Sine out"), _("Sine in/out"), + _("Circular in"), _("Circular out"), _("Circular in/out"), + _("Exponential in"), _("Exponential out"), _("Exponential in/out"), + _("Elastic in"), _("Elastic out"), _("Elastic in/out"), + _("Back in"), _("Back out"), _("Back in/out"), + _("Bounce in"), _("Bounce out"), _("Bounce in/out") + }, + { + "EaseNone", + "EaseQuadIn", "EaseQuadOut", "EaseQuadInOut", + "EaseCubicIn", "EaseCubicOut", "EaseCubicInOut", + "EaseQuartIn", "EaseQuartOut", "EaseQuartInOut", + "EaseQuintIn", "EaseQuintOut", "EaseQuintInOut", + "EaseSineIn", "EaseSineOut", "EaseSineInOut", + "EaseCircularIn", "EaseCircularOut", "EaseCircularInOut", + "EaseExponentialIn", "EaseExponentialOut", "EaseExponentialInOut", + "EaseElasticIn", "EaseElasticOut", "EaseElasticInOut", + "EaseBackIn", "EaseBackOut", "EaseBackInOut", + "EaseBounceIn", "EaseBounceOut", "EaseBounceInOut" + }, + 0, "death-easing"); + result.add_float(_("Death time"), &m_particle_death_time, "death-time", 5.f); + result.add_float(_("Death time variation"), &m_particle_death_time_variation, "death-time-variation", 0.f); + result.add_float(_("Speed X"), &m_particle_speed_x, "speed-x", 0.f); + result.add_float(_("Speed Y"), &m_particle_speed_y, "speed-y", 0.f); + result.add_float(_("Speed X (variation)"), &m_particle_speed_variation_x, "speed-var-x", 0.f); + result.add_float(_("Speed Y (variation)"), &m_particle_speed_variation_y, "speed-var-y", 0.f); + result.add_float(_("Acceleration X"), &m_particle_acceleration_x, "acceleration-x", 0.f); + result.add_float(_("Acceleration Y"), &m_particle_acceleration_y, "acceleration-y", 0.f); + result.add_float(_("Friction X"), &m_particle_friction_x, "friction-x", 0.f); + result.add_float(_("Friction Y"), &m_particle_friction_y, "friction-y", 0.f); + result.add_float(_("Feather factor"), &m_particle_feather_factor, "feather-factor", 0.f); + result.add_float(_("Rotation"), &m_particle_rotation, "rotation", 0.f); + result.add_float(_("Rotation (variation)"), &m_particle_rotation_variation, "rotation-variation", 0.f); + result.add_float(_("Rotation speed"), &m_particle_rotation_speed, "rotation-speed", 0.f); + result.add_float(_("Rotation speed (variation)"), &m_particle_rotation_speed_variation, "rotation-speed-variation", 0.f); + result.add_float(_("Rotation acceleration"), &m_particle_rotation_acceleration, "rotation-acceleration", 0.f); + result.add_float(_("Rotation friction"), &m_particle_rotation_decceleration, "rotation-decceleration", 0.f); + result.add_enum(_("Rotation mode"), reinterpret_cast(&m_particle_rotation_mode), + {_("Fixed"), _("Facing"), _("Wiggling")}, + {"fixed", "facing", "wiggling"}, + static_cast(RotationMode::Fixed), + "rotation-mode"); + result.add_enum(_("Collision mode"), reinterpret_cast(&m_particle_collision_mode), + {_("None (pass through)"), _("Stick"), _("Bounce (heavy)"), _("Bounce (light)"), _("Kill particle")}, + {"ignore", "stick", "bounce-heavy", "bounce-light", "destroy"}, + static_cast(CollisionMode::Ignore), + "collision-mode"); + result.add_enum(_("Delete if off-screen"), reinterpret_cast(&m_particle_offscreen_mode), + {_("Never"), _("Only on exit"), _("Always")}, + {"never", "only-on-exit", "always"}, + static_cast(OffscreenMode::Never), + "offscreen-mode"); + result.add_bool(_("Cover screen"), &m_cover_screen, "cover-screen", true); + + //result.reorder({"amount", "delay", "lifetime", "lifetime-variation", "enabled", "name"}); + + return result; +} + +void +CustomParticleSystem::update(float dt_sec) +{ + // "enabled" being false only means new particles shouldn't spawn; + // update the already existing particles regardless, if any + + // Update existing particles + for (auto& it : custom_particles) { + auto particle = dynamic_cast(it.get()); + assert(particle); + + if (particle->birth_time > dt_sec) { + switch(particle->birth_mode) { + case FadeMode::Shrink: + particle->scale = static_cast( + getEasingByName(particle->birth_easing)( + static_cast( + 1.f - (particle->birth_time / particle->total_birth) + ) + )); + break; + case FadeMode::Fade: + particle->props = SpriteProperties(particle->original_props, + 1.f - (particle->birth_time / + particle->total_birth)); + break; + default: + break; + } + particle->birth_time -= dt_sec; + } else if (particle->birth_time > 0.f) { + particle->birth_time = 0.f; + switch(particle->birth_mode) { + case FadeMode::Shrink: + particle->scale = 1.f; + break; + case FadeMode::Fade: + particle->props = particle->original_props; + break; + default: + break; + } + } + + particle->lifetime -= dt_sec; + if (particle->lifetime < 0.f) { + particle->lifetime = 0.f; + } + + if (particle->birth_time <= 0.f && particle->lifetime <= 0.f) { + if (particle->death_time > dt_sec) { + switch(particle->death_mode) { + case FadeMode::Shrink: + particle->scale = 1.f - static_cast( + getEasingByName(particle->death_easing)( + static_cast( + 1.f - (particle->death_time / particle->total_death) + ) + )); + break; + case FadeMode::Fade: + particle->props = SpriteProperties(particle->original_props, + (particle->death_time / + particle->total_death)); + break; + default: + break; + } + particle->death_time -= dt_sec; + } else { + particle->death_time = 0.f; + switch(particle->death_mode) { + case FadeMode::Shrink: + particle->scale = 0.f; + break; + case FadeMode::Fade: + particle->props = SpriteProperties(particle->original_props, 0.f); + break; + default: + break; + } + particle->ready_for_deletion = true; + } + } + + particle->speedX += graphicsRandom.randf(-particle->feather_factor, + particle->feather_factor) * dt_sec * 1000.f; + particle->speedY += graphicsRandom.randf(-particle->feather_factor, + particle->feather_factor) * dt_sec * 1000.f; + particle->speedX += particle->accX * dt_sec; + particle->speedY += particle->accY * dt_sec; + particle->speedX *= 1.f - particle->frictionX * dt_sec; + particle->speedY *= 1.f - particle->frictionY * dt_sec; + + if (collision(particle, + Vector(particle->speedX,particle->speedY) * dt_sec) > 0) { + switch(particle->collision_mode) { + case CollisionMode::Ignore: + particle->pos.x += particle->speedX * dt_sec; + particle->pos.y += particle->speedY * dt_sec; + break; + case CollisionMode::Stick: + + break; + case CollisionMode::BounceHeavy: + + break; + case CollisionMode::BounceLight: + + break; + case CollisionMode::Destroy: + particle->ready_for_deletion = true; + break; + } + } else { + particle->pos.x += particle->speedX * dt_sec; + particle->pos.y += particle->speedY * dt_sec; + } + + switch(particle->angle_mode) { + case RotationMode::Facing: + particle->angle = atan(particle->speedY / particle->speedX) * 180.f / PI; + break; + case RotationMode::Wiggling: + particle->angle += graphicsRandom.randf(-particle->angle_speed / 2.f, + particle->angle_speed / 2.f) * dt_sec; + break; + case RotationMode::Fixed: + default: + particle->angle_speed += particle->angle_acc * dt_sec; + particle->angle_speed *= 1.f - particle->angle_decc * dt_sec; + particle->angle += particle->angle_speed * dt_sec; + } + + float abs_x = Sector::get().get_camera().get_translation().x; + float abs_y = Sector::get().get_camera().get_translation().y; + + if (!particle->has_been_on_screen) { + if (particle->pos.y <= static_cast(SCREEN_HEIGHT) + abs_y + && particle->pos.y >= abs_y + && particle->pos.x <= static_cast(SCREEN_WIDTH) + abs_x + && particle->pos.x >= abs_x) { + particle->has_been_on_screen = true; + } + } + + switch(particle->offscreen_mode) { + case OffscreenMode::Always: + if (particle->pos.y > static_cast(SCREEN_HEIGHT) + abs_y + || particle->pos.y < abs_y + || particle->pos.x > static_cast(SCREEN_WIDTH) + abs_x + || particle->pos.x < abs_x) { + particle->ready_for_deletion = true; + } + break; + case OffscreenMode::OnlyOnExit: + if ((particle->pos.y > static_cast(SCREEN_HEIGHT) + abs_y + || particle->pos.y < abs_y + || particle->pos.x > static_cast(SCREEN_WIDTH) + abs_x + || particle->pos.x < abs_x) + && particle->has_been_on_screen) { + particle->ready_for_deletion = true; + } + break; + case OffscreenMode::Never: + break; + } + + bool is_in_life_zone = false; + for (auto& zone : GameSession::current()->get_current_sector().get_objects_by_type()) { + if (zone.get_rect().contains(particle->pos) && zone.get_particle_name() == m_name) { + switch(zone.get_type()) { + case ParticleZone::ParticleZoneType::Killer: + particle->lifetime = 0.f; + particle->birth_time = 0.f; + break; + + case ParticleZone::ParticleZoneType::Destroyer: + particle->ready_for_deletion = true; + break; + + case ParticleZone::ParticleZoneType::LifeClear: + particle->last_life_zone_required_instakill = true; + particle->has_been_in_life_zone = true; + is_in_life_zone = true; + break; + + case ParticleZone::ParticleZoneType::Life: + particle->last_life_zone_required_instakill = false; + particle->has_been_in_life_zone = true; + is_in_life_zone = true; + break; + + // Nothing to do; there's a warning if I don't put that here + case ParticleZone::ParticleZoneType::Spawn: + break; + } + } + } // For each ParticleZone object + + if (!is_in_life_zone && particle->has_been_in_life_zone) { + if (particle->last_life_zone_required_instakill) { + particle->ready_for_deletion = true; + } else { + particle->lifetime = 0.f; + particle->birth_time = 0.f; + } + } + + } // For each particle + + // Clear dead particles + // Scroll through the vector backwards, because removing an element affects + // the index of all elements after it (prevents buggy behavior) + for (int i = static_cast(custom_particles.size()) - 1; i >= 0; --i) { + auto particle = dynamic_cast(custom_particles.at(i).get()); + + if (particle->ready_for_deletion) + custom_particles.erase(custom_particles.begin()+i); + } + + // Add necessary particles + float remaining = dt_sec + time_last_remaining; + + if (enabled) { + int real_max = m_max_amount; + if (!m_cover_screen) { + int i = 0; + for (auto& zone : GameSession::current()->get_current_sector().get_objects_by_type()) { + if (zone.get_type() == ParticleZone::ParticleZoneType::Spawn && zone.get_particle_name() == m_name) { + i++; + } + } + real_max *= i; + } + while (remaining > m_delay && int(custom_particles.size()) < real_max) + { + spawn_particles(remaining); + remaining -= m_delay; + } + } + + // Maxes to m_delay, so that if there's already the max amount of particles, + // it won't store all the time waiting for some particles to go and then + // spawn a bazillion particles instantly. (Bacisally it means : This will + // help guarantee there will be at least m_delay between each particle + // spawn as long as m_delay >= dt_sec) + time_last_remaining = (remaining > m_delay) ? m_delay : remaining; + +} + +void +CustomParticleSystem::draw(DrawingContext& context) +{ + // "enabled" being false only means new particles shouldn't spawn; + // draw the already existing particles regardless, if any + + context.push_transform(); + + std::unordered_map batches; + for (const auto& particle : custom_particles) { + auto it = batches.find(&(particle->props)); + if (it == batches.end()) { + const auto& batch_it = batches.emplace(&(particle->props), + SurfaceBatch(particle->props.texture, particle->props.color)); + batch_it.first->second.draw(Rectf(particle->pos, + Vector( + particle->pos.x + particle->scale + * static_cast( + particle->props.texture->get_width() + ) * particle->props.scale.x, + particle->pos.y + particle->scale + * static_cast( + particle->props.texture->get_height() + ) * particle->props.scale.y + ) + ), particle->angle); + } else { + it->second.draw(Rectf(particle->pos, + Vector( + particle->pos.x + particle->scale + * static_cast( + particle->texture->get_width() + ) * particle->props.scale.x, + particle->pos.y + particle->scale + * static_cast( + particle->texture->get_height() + ) * particle->props.scale.y + ) + ), particle->angle); + } + } + + for(auto& it : batches) { + auto& surface = it.first->texture; + auto& batch = it.second; + // FIXME: What is the colour used for? + context.color().draw_surface_batch(surface, batch.move_srcrects(), + batch.move_dstrects(), batch.move_angles(), it.first->color, z_pos); + } + + context.pop_transform(); +} + +// Duplicated from ParticleSystem_Interactive because I intend to bring edits +// sometime in the future, for even more flexibility with particles. (Semphris) +int +CustomParticleSystem::collision(Particle* object, const Vector& movement) +{ + using namespace collision; + + CustomParticle* particle = dynamic_cast(object); + assert(particle); + + // calculate rectangle where the object will move + float x1, x2; + float y1, y2; + + x1 = object->pos.x; + x2 = x1 + particle->props.scale.x * static_cast(particle->props.texture->get_width()) + movement.x; + if (x2 < x1) { + x1 = x2; + x2 = object->pos.x; + } + + y1 = object->pos.y; + y2 = y1 + particle->props.scale.y * static_cast(particle->props.texture->get_height()) + movement.y; + if (y2 < y1) { + y1 = y2; + y2 = object->pos.y; + } + bool water = false; + + // test with all tiles in this rectangle + int starttilex = int(x1-1) / 32; + int starttiley = int(y1-1) / 32; + int max_x = int(x2+1); + int max_y = int(y2+1); + + Rectf dest(x1, y1, x2, y2); + dest.move(movement); + Constraints constraints; + + for (const auto& solids : Sector::get().get_solid_tilemaps()) { + // FIXME Handle a nonzero tilemap offset + for (int x = starttilex; x*32 < max_x; ++x) { + for (int y = starttiley; y*32 < max_y; ++y) { + const Tile& tile = solids->get_tile(x, y); + + // skip non-solid tiles, except water + if (! (tile.get_attributes() & (Tile::WATER | Tile::SOLID))) + continue; + + Rectf rect = solids->get_tile_bbox(x, y); + if (tile.is_slope ()) { // slope tile + AATriangle triangle = AATriangle(rect, tile.get_data()); + + if (rectangle_aatriangle(&constraints, dest, triangle)) { + if (tile.get_attributes() & Tile::WATER) + water = true; + } + } else { // normal rectangular tile + if (intersects(dest, rect)) { + if (tile.get_attributes() & Tile::WATER) + water = true; + set_rectangle_rectangle_constraints(&constraints, dest, rect); + } + } + } + } + } + + // TODO don't use magic numbers here... + + // did we collide at all? + if (!constraints.has_constraints()) + return -1; + + const CollisionHit& hit = constraints.hit; + if (water) { + return 0; //collision with water tile - don't draw splash + } else { + if (hit.right || hit.left) { + return 2; //collision from right + } else { + return 1; //collision from above + } + } +} + + +// ============================================================================= +// LOCAL + +CustomParticleSystem::SpriteProperties +CustomParticleSystem::get_random_texture() +{ + float val = graphicsRandom.randf(texture_sum_odds); + for (auto texture : m_textures) + { + val -= texture.likeliness; + if (val <= 0) + { + return texture; + } + } + return m_textures.at(0); +} + +/** Initializes and adds a single particle to the stack. Performs + * no check regarding the maximum amount of total particles. + * @param lifetime The time elapsed since the moment the particle should have been born + */ +void +CustomParticleSystem::add_particle(float lifetime, float x, float y) +{ + auto particle = std::make_unique(); + particle->original_props = get_random_texture(); + particle->props = particle->original_props; + + particle->pos.x = x; + particle->pos.y = y; + + float life_elapsed = lifetime; + float birth_delta = m_particle_birth_time_variation / 2; + particle->total_birth = m_particle_birth_time + graphicsRandom.randf(-birth_delta, birth_delta); + particle->birth_time = particle->total_birth - life_elapsed; + if (particle->birth_time < 0.f) { + life_elapsed = -particle->birth_time; + particle->birth_time = 0.f; + } else { + life_elapsed = 0.f; + } + float life_delta = m_particle_lifetime_variation / 2; + particle->lifetime = m_particle_lifetime - life_elapsed + graphicsRandom.randf(-life_delta, life_delta); + if (particle->lifetime < 0.f) { + life_elapsed = -particle->lifetime; + particle->lifetime = 0.f; + } else { + life_elapsed = 0.f; + } + float death_delta = m_particle_death_time_variation / 2; + particle->total_death = m_particle_death_time + graphicsRandom.randf(-death_delta, death_delta); + particle->death_time = particle->total_death - life_elapsed; + + particle->birth_mode = m_particle_birth_mode; + particle->death_mode = m_particle_death_mode; + + particle->birth_easing = m_particle_birth_easing; + particle->death_easing = m_particle_death_easing; + + switch(particle->birth_mode) { + case FadeMode::Shrink: + particle->scale = 0.f; + break; + default: + break; + } + + float speedx_delta = m_particle_speed_variation_x / 2; + particle->speedX = m_particle_speed_x + graphicsRandom.randf(-speedx_delta, speedx_delta); + float speedy_delta = m_particle_speed_variation_y / 2; + particle->speedY = m_particle_speed_y + graphicsRandom.randf(-speedy_delta, speedy_delta); + particle->accX = m_particle_acceleration_x; + particle->accY = m_particle_acceleration_y; + particle->frictionX = m_particle_friction_x; + particle->frictionY = m_particle_friction_y; + + particle->feather_factor = m_particle_feather_factor; + + float angle_delta = m_particle_rotation_variation / 2; + particle->angle = m_particle_rotation + graphicsRandom.randf(-angle_delta, angle_delta); + float angle_speed_delta = m_particle_rotation_speed_variation / 2; + particle->angle_speed = m_particle_rotation_speed + graphicsRandom.randf(-angle_speed_delta, angle_speed_delta); + particle->angle_acc = m_particle_rotation_acceleration; + particle->angle_decc = m_particle_rotation_decceleration; + particle->angle_mode = m_particle_rotation_mode; + + particle->collision_mode = m_particle_collision_mode; + + particle->offscreen_mode = m_particle_offscreen_mode; + + custom_particles.push_back(std::move(particle)); +} + +void +CustomParticleSystem::spawn_particles(float lifetime) +{ + if (!m_cover_screen) { + for (auto& zone : GameSession::current()->get_current_sector().get_objects_by_type()) { + if (zone.get_type() == ParticleZone::ParticleZoneType::Spawn && zone.get_particle_name() == m_name) { + Rectf rect = zone.get_rect(); + add_particle(lifetime, + graphicsRandom.randf(rect.get_width()) + rect.get_left(), + graphicsRandom.randf(rect.get_height()) + rect.get_top()); + } + } + } else { + float abs_x = Sector::get().get_camera().get_translation().x; + float abs_y = Sector::get().get_camera().get_translation().y; + add_particle(lifetime, + graphicsRandom.randf(virtual_width) + abs_x, + graphicsRandom.randf(virtual_height) + abs_y); + } +} + +// SCRIPTING + + + +/* EOF */ diff --git a/src/object/custom_particle_system.hpp b/src/object/custom_particle_system.hpp new file mode 100644 index 00000000000..1c14f87c85a --- /dev/null +++ b/src/object/custom_particle_system.hpp @@ -0,0 +1,235 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_OBJECT_CUSTOM_PARTICLE_SYSTEM_HPP +#define HEADER_SUPERTUX_OBJECT_CUSTOM_PARTICLE_SYSTEM_HPP + +#include "math/easing.hpp" +#include "math/vector.hpp" +#include "object/particlesystem_interactive.hpp" +#include "scripting/custom_particles.hpp" +#include "video/surface.hpp" +#include "video/surface_ptr.hpp" + +class CustomParticleSystem final : + public ParticleSystem_Interactive, + public ExposedObject +{ +public: + CustomParticleSystem(); + CustomParticleSystem(const ReaderMapping& reader); + virtual ~CustomParticleSystem(); + + virtual void draw(DrawingContext& context) override; + + void init(); + virtual void update(float dt_sec) override; + + virtual std::string get_class() const override { return "particles-custom"; } + virtual std::string get_display_name() const override { return _("Custom Particles"); } + virtual ObjectSettings get_settings() override; + + virtual const std::string get_icon_path() const override { + return "images/engine/editor/sparkle.png"; + } + + //void fade_amount(int new_amount, float fade_time); +protected: + virtual int collision(Particle* particle, const Vector& movement) override; + +private: + + // Local + void add_particle(float lifetime, float x, float y); + void spawn_particles(float lifetime); + + float texture_sum_odds; + float time_last_remaining; + + // Scripting + +private: + enum class RotationMode { + Fixed, + Facing, + Wiggling + }; + + enum class FadeMode { + None, + Fade, + Shrink + }; + + enum class CollisionMode { + Ignore, + Stick, + BounceHeavy, + BounceLight, + Destroy + }; + + enum class OffscreenMode { + Never, + OnlyOnExit, + Always + }; + + class SpriteProperties final + { + public: + float likeliness; + Color color; + SurfacePtr texture; + Vector scale; + + SpriteProperties() : + likeliness(1.f), + color(1.f, 1.f, 1.f, 1.f), + texture(Surface::from_file("images/engine/editor/sparkle.png")), + scale(1.f, 1.f) + { + } + + SpriteProperties(SurfacePtr surface) : + likeliness(1.f), + color(1.f, 1.f, 1.f, 1.f), + texture(surface), + scale(1.f, 1.f) + { + } + + SpriteProperties(SpriteProperties& sp, float alpha) : + likeliness(sp.likeliness), + color(sp.color.red, sp.color.green, sp.color.blue, sp.color.alpha * alpha), + texture(sp.texture), + scale(sp.scale) + { + } + + inline bool operator==(const SpriteProperties& sp) + { + return this->likeliness == sp.likeliness + && this->color == sp.color + && this->texture == sp.texture + && this->scale == sp.scale; + } + inline bool operator!=(const SpriteProperties& sp) + { + return !operator==(sp); + } + }; + + SpriteProperties get_random_texture(); + + class CustomParticle : public Particle + { + public: + SpriteProperties original_props, props; + float lifetime, birth_time, death_time, + total_birth, total_death; + FadeMode birth_mode, death_mode; + EasingMode birth_easing, death_easing; + bool ready_for_deletion; + float speedX, speedY, + accX, accY, + frictionX, frictionY; + float feather_factor; + float angle_speed, angle_acc, + angle_decc; + RotationMode angle_mode; + CollisionMode collision_mode; + OffscreenMode offscreen_mode; + bool has_been_on_screen; + bool has_been_in_life_zone; + bool last_life_zone_required_instakill; + + CustomParticle() : + original_props(), + props(), + lifetime(), + birth_time(), + death_time(), + total_birth(), + total_death(), + birth_mode(), + death_mode(), + birth_easing(), + death_easing(), + ready_for_deletion(false), + speedX(), + speedY(), + accX(), + accY(), + frictionX(), + frictionY(), + feather_factor(), + angle_speed(), + angle_acc(), + angle_decc(), + angle_mode(), + collision_mode(), + offscreen_mode(), + has_been_on_screen(), + has_been_in_life_zone(false), + last_life_zone_required_instakill(false) + {} + }; + + std::vector m_textures; + std::vector > custom_particles; + + std::string m_particle_main_texture; + int m_max_amount; + float m_delay; + float m_particle_lifetime; + float m_particle_lifetime_variation; + float m_particle_birth_time, + m_particle_birth_time_variation, + m_particle_death_time, + m_particle_death_time_variation; + FadeMode m_particle_birth_mode, + m_particle_death_mode; + EasingMode m_particle_birth_easing, + m_particle_death_easing; + float m_particle_speed_x, + m_particle_speed_y, + m_particle_speed_variation_x, + m_particle_speed_variation_y, + m_particle_acceleration_x, + m_particle_acceleration_y, + m_particle_friction_x, + m_particle_friction_y; + float m_particle_feather_factor; + float m_particle_rotation, + m_particle_rotation_variation, + m_particle_rotation_speed, + m_particle_rotation_speed_variation, + m_particle_rotation_acceleration, + m_particle_rotation_decceleration; + RotationMode m_particle_rotation_mode; + CollisionMode m_particle_collision_mode; + OffscreenMode m_particle_offscreen_mode; + bool m_cover_screen; + +private: + CustomParticleSystem(const CustomParticleSystem&) = delete; + CustomParticleSystem& operator=(const CustomParticleSystem&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/object/display_effect.cpp b/src/object/display_effect.cpp index 6d684b994e4..958c84ea770 100644 --- a/src/object/display_effect.cpp +++ b/src/object/display_effect.cpp @@ -24,10 +24,10 @@ static const float BORDER_SIZE = 75; DisplayEffect::DisplayEffect(const std::string& name) : GameObject(name), ExposedObject(this), - screen_fade(NO_FADE), + screen_fade(FadeType::NO_FADE), screen_fadetime(0), screen_fading(0), - border_fade(NO_FADE), + border_fade(FadeType::NO_FADE), border_fadetime(0), border_fading(), border_size(0), @@ -44,18 +44,18 @@ void DisplayEffect::update(float dt_sec) { switch (screen_fade) { - case NO_FADE: + case FadeType::NO_FADE: break; - case FADE_IN: + case FadeType::FADE_IN: screen_fading -= dt_sec; if (screen_fading < 0) { - screen_fade = NO_FADE; + screen_fade = FadeType::NO_FADE; } break; - case FADE_OUT: + case FadeType::FADE_OUT: screen_fading -= dt_sec; if (screen_fading < 0) { - screen_fade = NO_FADE; + screen_fade = FadeType::NO_FADE; black = true; } break; @@ -64,21 +64,21 @@ DisplayEffect::update(float dt_sec) } switch (border_fade) { - case NO_FADE: + case FadeType::NO_FADE: break; - case FADE_IN: + case FadeType::FADE_IN: border_fading -= dt_sec; if (border_fading < 0) { - border_fade = NO_FADE; + border_fade = FadeType::NO_FADE; } border_size = (border_fadetime - border_fading) / border_fadetime * BORDER_SIZE; break; - case FADE_OUT: + case FadeType::FADE_OUT: border_fading -= dt_sec; if (border_fading < 0) { borders = false; - border_fade = NO_FADE; + border_fade = FadeType::NO_FADE; } border_size = border_fading / border_fadetime * BORDER_SIZE; break; @@ -93,16 +93,16 @@ DisplayEffect::draw(DrawingContext& context) context.push_transform(); context.set_translation(Vector(0, 0)); - if (black || screen_fade != NO_FADE) { + if (black || screen_fade != FadeType::NO_FADE) { float alpha; if (black) { alpha = 1.0f; } else { switch (screen_fade) { - case FADE_IN: + case FadeType::FADE_IN: alpha = screen_fading / screen_fadetime; break; - case FADE_OUT: + case FadeType::FADE_OUT: alpha = (screen_fadetime - screen_fading) / screen_fadetime; break; default: @@ -140,7 +140,7 @@ DisplayEffect::fade_out(float fadetime) black = false; screen_fadetime = fadetime; screen_fading = fadetime; - screen_fade = FADE_OUT; + screen_fade = FadeType::FADE_OUT; } void @@ -149,7 +149,7 @@ DisplayEffect::fade_in(float fadetime) black = false; screen_fadetime = fadetime; screen_fading = fadetime; - screen_fade = FADE_IN; + screen_fade = FadeType::FADE_IN; } void @@ -173,7 +173,7 @@ DisplayEffect::sixteen_to_nine(float fadetime) } else { borders = true; border_size = 0; - border_fade = FADE_IN; + border_fade = FadeType::FADE_IN; border_fadetime = fadetime; border_fading = border_fadetime; } @@ -186,7 +186,7 @@ DisplayEffect::four_to_three(float fadetime) borders = false; } else { border_size = BORDER_SIZE; - border_fade = FADE_OUT; + border_fade = FadeType::FADE_OUT; border_fadetime = fadetime; border_fading = border_fadetime; } diff --git a/src/object/display_effect.hpp b/src/object/display_effect.hpp index 331338988a8..ddf29d8c545 100644 --- a/src/object/display_effect.hpp +++ b/src/object/display_effect.hpp @@ -46,7 +46,7 @@ class DisplayEffect final : public GameObject, /** @} */ private: - enum FadeType { + enum class FadeType { NO_FADE, FADE_IN, FADE_OUT }; diff --git a/src/object/particle_zone.cpp b/src/object/particle_zone.cpp new file mode 100644 index 00000000000..6f72ace59ed --- /dev/null +++ b/src/object/particle_zone.cpp @@ -0,0 +1,133 @@ +// SuperTux - Particle spawn zone +// Copyright (C) 2020 A. Semphris +// +// 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 "object/particle_zone.hpp" + +#include "editor/editor.hpp" +#include "util/reader_mapping.hpp" +#include "video/drawing_context.hpp" + + +ParticleZone::ParticleZone(const ReaderMapping& reader) : + MovingObject(reader), + //ExposedObject(this), + m_enabled(), + m_particle_name(), + m_type() +{ + float w,h; + reader.get("x", m_col.m_bbox.get_left(), 0.0f); + reader.get("y", m_col.m_bbox.get_top(), 0.0f); + reader.get("width", w, 32.0f); + reader.get("height", h, 32.0f); + m_col.m_bbox.set_size(w, h); + + reader.get("enabled", m_enabled, true); + reader.get("particle-name", m_particle_name, ""); + + std::string zone_type; + if (reader.get("zone-type", zone_type)) + { + if (zone_type == "destroyer") + { + m_type = ParticleZoneType::Destroyer; + } + else if (zone_type == "killer") + { + m_type = ParticleZoneType::Killer; + } + else if (zone_type == "life") + { + m_type = ParticleZoneType::Life; + } + else if (zone_type == "life-clear") + { + m_type = ParticleZoneType::LifeClear; + } + else + { + m_type = ParticleZoneType::Spawn; + } + } + else + { + m_type = ParticleZoneType::Spawn; + } + + set_group(COLGROUP_TOUCHABLE); +} + +ObjectSettings +ParticleZone::get_settings() +{ + ObjectSettings result = MovingObject::get_settings(); + + result.add_bool(_("Enabled"), &m_enabled, "enabled", true); + result.add_text(_("Particle Name"), &m_particle_name, "particle-name"); + result.add_enum(_("Zone Type"), reinterpret_cast(&m_type), + {_("Spawn"), _("Life zone"), _("Life zone (clear)"), _("Kill particles"), _("Clear particles")}, + {"spawn", "life", "life-clear", "killer", "destroyer"}, + static_cast(ParticleZoneType::Spawn), + "zone-type"); + + result.reorder({"region", "name", "x", "y"}); + + return result; +} + +void +ParticleZone::update(float dt_sec) +{ + // This object doesn't manage creating particles :) + // See `src/object/custom_particle_system.*pp` for that +} + +void +ParticleZone::draw(DrawingContext& context) +{ + if (Editor::is_active()) { + switch(m_type) { + case ParticleZoneType::Spawn: + context.color().draw_filled_rect(m_col.m_bbox, Color(0.5f, 0.5f, 1.0f, 0.6f), + 0.0f, LAYER_OBJECTS); + break; + case ParticleZoneType::Life: + context.color().draw_filled_rect(m_col.m_bbox, Color(0.5f, 1.0f, 0.5f, 0.6f), + 0.0f, LAYER_OBJECTS); + break; + case ParticleZoneType::LifeClear: + context.color().draw_filled_rect(m_col.m_bbox, Color(1.0f, 1.0f, 0.5f, 0.6f), + 0.0f, LAYER_OBJECTS); + break; + case ParticleZoneType::Killer: + context.color().draw_filled_rect(m_col.m_bbox, Color(1.0f, 0.75f, 0.5f, 0.6f), + 0.0f, LAYER_OBJECTS); + break; + case ParticleZoneType::Destroyer: + context.color().draw_filled_rect(m_col.m_bbox, Color(1.0f, 0.5f, 0.5f, 0.6f), + 0.0f, LAYER_OBJECTS); + break; + } + } +} + +HitResponse +ParticleZone::collision(GameObject& other, const CollisionHit& hit) +{ + return ABORT_MOVE; +} + +/* EOF */ diff --git a/src/object/particle_zone.hpp b/src/object/particle_zone.hpp new file mode 100644 index 00000000000..a904d1671b5 --- /dev/null +++ b/src/object/particle_zone.hpp @@ -0,0 +1,113 @@ +// SuperTux - Particle zone : Spawn +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_OBJECT_PARTICLE_ZONE_HPP +#define HEADER_SUPERTUX_OBJECT_PARTICLE_ZONE_HPP + +#include "squirrel/exposed_object.hpp" +// TODO: #include "scripting/wind.hpp" +#include "supertux/moving_object.hpp" + +class ReaderMapping; + +/** Defines an area where a certain particle type can spawn */ +class ParticleZone final : + public MovingObject//, // TODO: Make this area actually moveable with Squirrel + //public ExposedObject // TODO: Scripting interface +{ +public: + ParticleZone(const ReaderMapping& reader); + + virtual void update(float dt_sec) override; + virtual void draw(DrawingContext& context) override; + + virtual bool has_variable_size() const override { return true; } + virtual std::string get_class() const override { return "particle-zone"; } + virtual std::string get_display_name() const override { return _("Particle zone");} + virtual HitResponse collision(GameObject& other, const CollisionHit& hit) override; + + virtual ObjectSettings get_settings() override; + + Rectf get_rect() {return m_col.m_bbox;} + + enum class ParticleZoneType { + /** Particles will spawn in this area */ + Spawn, + /** TODO: Particles will die if they leave this area */ + Life, + /** TODO: Particles will disappear instantly if they leave this area */ + LifeClear, + /** Particles will start dying if they touch this area */ + Killer, + /** Particles will disappear instantly if they touch this area */ + Destroyer + }; + + /** @name Scriptable Methods + @{ */ + + /** Sets whether or not particles can spawn in this area */ + void set_enabled(bool enabled) {m_enabled = enabled;} + + /** Returns whether or not particles can spawn in this area */ + bool get_enabled() const {return m_enabled;} + + /** Sets the name of the particle object for this area */ + void set_particle_name(std::string& particle_name) {m_particle_name = particle_name;} + + /** Returns the name of the particle object for this area */ + std::string get_particle_name() const {return m_particle_name;} + + /** Move the area around. Multiple calls stack (e. g. calling one before + * the other finished will play both movements simultaneously) + */ + //void displace(int x, int y, float time, std::string easing); + + /** Resize the area. Multiple calls stack (e. g. calling one before + * the other finished will play both resizes simultaneously) + */ + //void resize(int width, int height, float time, std::string easing); + + /** Returns the current X position of the zone */ + float current_x() {return m_col.m_bbox.get_left();} + + /** Returns the current Y position of the zone */ + float current_y() {return m_col.m_bbox.get_top();} + + /** Returns the target X position of the zone */ + //float target_x() {return m_col.m_bbox.get_left();} + + /** Returns the target Y position of the zone */ + //float target_y() {return m_col.m_bbox.get_left();} + + /** @} */ + + void set_type(ParticleZoneType type) {m_type = type;} + ParticleZoneType get_type() {return m_type;} + +private: + bool m_enabled; + std::string m_particle_name; + ParticleZoneType m_type; + +private: + ParticleZone(const ParticleZone&) = delete; + ParticleZone& operator=(const ParticleZone&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/object/particlesystem.hpp b/src/object/particlesystem.hpp index 08745058e00..c9bb93e9d58 100644 --- a/src/object/particlesystem.hpp +++ b/src/object/particlesystem.hpp @@ -70,7 +70,8 @@ class ParticleSystem : public GameObject//, pos(), angle(), texture(), - alpha() + alpha(), + scale(1.f) // This currently only works in the custom particle system {} virtual ~Particle() @@ -81,6 +82,7 @@ class ParticleSystem : public GameObject//, float angle; SurfacePtr texture; float alpha; + float scale; // see initializer private: Particle(const Particle&) = delete; diff --git a/src/object/particlesystem_interactive.hpp b/src/object/particlesystem_interactive.hpp index 70238a06176..bbc11bc268f 100644 --- a/src/object/particlesystem_interactive.hpp +++ b/src/object/particlesystem_interactive.hpp @@ -49,7 +49,7 @@ class ParticleSystem_Interactive : public ParticleSystem } protected: - int collision(Particle* particle, const Vector& movement); + virtual int collision(Particle* particle, const Vector& movement); private: ParticleSystem_Interactive(const ParticleSystem_Interactive&) = delete; diff --git a/src/object/path.cpp b/src/object/path.cpp index bc0e41976bb..89a7401a168 100644 --- a/src/object/path.cpp +++ b/src/object/path.cpp @@ -19,11 +19,11 @@ #include "object/path.hpp" #include "editor/node_marker.hpp" +#include "math/easing.hpp" #include "supertux/sector.hpp" #include "util/reader_mapping.hpp" #include "util/writer.hpp" #include "util/log.hpp" -#include "math/easing.hpp" WalkMode string_to_walk_mode(const std::string& mode_string) diff --git a/src/object/rain_particle_system.cpp b/src/object/rain_particle_system.cpp index ee4c54949c8..232c1c2ed9e 100644 --- a/src/object/rain_particle_system.cpp +++ b/src/object/rain_particle_system.cpp @@ -190,7 +190,7 @@ void RainParticleSystem::update(float dt_sec) } else { m_angle_fade_time_remaining -= dt_sec; float progress = 1.f - m_angle_fade_time_remaining / m_angle_fade_time_total; - progress = static_cast(m_angle_easing(progress)); + progress = static_cast(m_angle_easing(static_cast(progress))); m_current_angle = progress * (m_target_angle - m_begin_angle) + m_begin_angle; } set_angle(m_current_angle); diff --git a/src/scripting/custom_particles.cpp b/src/scripting/custom_particles.cpp new file mode 100644 index 00000000000..e5db41b2e7b --- /dev/null +++ b/src/scripting/custom_particles.cpp @@ -0,0 +1,54 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "object/custom_particle_system.hpp" +#include "scripting/custom_particles.hpp" + +namespace scripting { + +void CustomParticles::set_enabled(bool enable) +{ + SCRIPT_GUARD_VOID; + object.set_enabled(enable); +} + +bool CustomParticles::get_enabled() const +{ + SCRIPT_GUARD_DEFAULT; + return object.get_enabled(); +} + +void CustomParticles::fade_speed(float speed, float time) +{ + //SCRIPT_GUARD_VOID; + //object.fade_speed(speed, time); +} + +void CustomParticles::fade_amount(int amount, float time, float time_between) +{ + //SCRIPT_GUARD_VOID; + //object.fade_amount(amount, time, time_between); +} + +void CustomParticles::set_amount(int amount, float time) +{ + //SCRIPT_GUARD_VOID; + //object.fade_amount(amount, time); +} + +} // namespace scripting + +/* EOF */ diff --git a/src/scripting/custom_particles.hpp b/src/scripting/custom_particles.hpp new file mode 100644 index 00000000000..2f06c927fcd --- /dev/null +++ b/src/scripting/custom_particles.hpp @@ -0,0 +1,60 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_SCRIPTING_CUSTOM_PARTICLES_HPP +#define HEADER_SUPERTUX_SCRIPTING_CUSTOM_PARTICLES_HPP + +#ifndef SCRIPTING_API +#include "scripting/game_object.hpp" + +class CustomParticleSystem; +#endif + +namespace scripting { + +class CustomParticles final +#ifndef SCRIPTING_API + : public GameObject<::CustomParticleSystem> +#endif +{ +#ifndef SCRIPTING_API +public: + using GameObject::GameObject; + +private: + CustomParticles(const CustomParticles&) = delete; + CustomParticles& operator=(const CustomParticles&) = delete; +#endif + +public: + void set_enabled(bool enable); + bool get_enabled() const; + + /** Smoothly changes the rain speed to the given value */ + void fade_speed(float speed, float time); + + /** Smoothly changes the amount of particles to the given value */ + void fade_amount(int amount, float time, float time_between); + + /** Smoothly changes the amount of particles to the given value */ + void set_amount(int amount, float time); +}; + +} // namespace scripting + +#endif + +/* EOF */ diff --git a/src/scripting/wrapper.cpp b/src/scripting/wrapper.cpp index 7888f96121f..cd010bbf395 100644 --- a/src/scripting/wrapper.cpp +++ b/src/scripting/wrapper.cpp @@ -840,6 +840,199 @@ static SQInteger Clouds_set_amount_wrapper(HSQUIRRELVM vm) } +static SQInteger CustomParticles_release_hook(SQUserPointer ptr, SQInteger ) +{ + auto _this = reinterpret_cast (ptr); + delete _this; + return 0; +} + +static SQInteger CustomParticles_set_enabled_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'set_enabled' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQBool arg0; + if(SQ_FAILED(sq_getbool(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a bool")); + return SQ_ERROR; + } + + try { + _this->set_enabled(arg0 == SQTrue); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'set_enabled'")); + return SQ_ERROR; + } + +} + +static SQInteger CustomParticles_get_enabled_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'get_enabled' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + + try { + bool return_value = _this->get_enabled(); + + sq_pushbool(vm, return_value); + return 1; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'get_enabled'")); + return SQ_ERROR; + } + +} + +static SQInteger CustomParticles_fade_speed_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'fade_speed' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQFloat arg0; + if(SQ_FAILED(sq_getfloat(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not a float")); + return SQ_ERROR; + } + SQFloat arg1; + if(SQ_FAILED(sq_getfloat(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a float")); + return SQ_ERROR; + } + + try { + _this->fade_speed(static_cast (arg0), static_cast (arg1)); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'fade_speed'")); + return SQ_ERROR; + } + +} + +static SQInteger CustomParticles_fade_amount_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'fade_amount' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQInteger arg0; + if(SQ_FAILED(sq_getinteger(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not an integer")); + return SQ_ERROR; + } + SQFloat arg1; + if(SQ_FAILED(sq_getfloat(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a float")); + return SQ_ERROR; + } + SQFloat arg2; + if(SQ_FAILED(sq_getfloat(vm, 4, &arg2))) { + sq_throwerror(vm, _SC("Argument 3 not a float")); + return SQ_ERROR; + } + + try { + _this->fade_amount(static_cast (arg0), static_cast (arg1), static_cast (arg2)); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'fade_amount'")); + return SQ_ERROR; + } + +} + +static SQInteger CustomParticles_set_amount_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'set_amount' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + SQInteger arg0; + if(SQ_FAILED(sq_getinteger(vm, 2, &arg0))) { + sq_throwerror(vm, _SC("Argument 1 not an integer")); + return SQ_ERROR; + } + SQFloat arg1; + if(SQ_FAILED(sq_getfloat(vm, 3, &arg1))) { + sq_throwerror(vm, _SC("Argument 2 not a float")); + return SQ_ERROR; + } + + try { + _this->set_amount(static_cast (arg0), static_cast (arg1)); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'set_amount'")); + return SQ_ERROR; + } + +} + static SQInteger Decal_release_hook(SQUserPointer ptr, SQInteger ) { auto _this = reinterpret_cast (ptr); @@ -7519,6 +7712,32 @@ void create_squirrel_instance(HSQUIRRELVM v, scripting::Clouds* object, bool set sq_remove(v, -2); // remove root table } +void create_squirrel_instance(HSQUIRRELVM v, scripting::CustomParticles* object, bool setup_releasehook) +{ + using namespace wrapper; + + sq_pushroottable(v); + sq_pushstring(v, "CustomParticles", -1); + if(SQ_FAILED(sq_get(v, -2))) { + std::ostringstream msg; + msg << "Couldn't resolved squirrel type 'CustomParticles'"; + throw SquirrelError(v, msg.str()); + } + + if(SQ_FAILED(sq_createinstance(v, -1)) || SQ_FAILED(sq_setinstanceup(v, -1, object))) { + std::ostringstream msg; + msg << "Couldn't setup squirrel instance for object of type 'CustomParticles'"; + throw SquirrelError(v, msg.str()); + } + sq_remove(v, -2); // remove object name + + if(setup_releasehook) { + sq_setreleasehook(v, -1, CustomParticles_release_hook); + } + + sq_remove(v, -2); // remove root table +} + void create_squirrel_instance(HSQUIRRELVM v, scripting::Decal* object, bool setup_releasehook) { using namespace wrapper; @@ -8653,6 +8872,52 @@ void register_supertux_wrapper(HSQUIRRELVM v) throw SquirrelError(v, "Couldn't register class 'Clouds'"); } + // Register class CustomParticles + sq_pushstring(v, "CustomParticles", -1); + if(sq_newclass(v, SQFalse) < 0) { + std::ostringstream msg; + msg << "Couldn't create new class 'CustomParticles'"; + throw SquirrelError(v, msg.str()); + } + sq_pushstring(v, "set_enabled", -1); + sq_newclosure(v, &CustomParticles_set_enabled_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tb"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'set_enabled'"); + } + + sq_pushstring(v, "get_enabled", -1); + sq_newclosure(v, &CustomParticles_get_enabled_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|t"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'get_enabled'"); + } + + sq_pushstring(v, "fade_speed", -1); + sq_newclosure(v, &CustomParticles_fade_speed_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tnn"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'fade_speed'"); + } + + sq_pushstring(v, "fade_amount", -1); + sq_newclosure(v, &CustomParticles_fade_amount_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tinn"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'fade_amount'"); + } + + sq_pushstring(v, "set_amount", -1); + sq_newclosure(v, &CustomParticles_set_amount_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|tin"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'set_amount'"); + } + + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register class 'CustomParticles'"); + } + // Register class Decal sq_pushstring(v, "Decal", -1); if(sq_newclass(v, SQFalse) < 0) { diff --git a/src/scripting/wrapper.hpp b/src/scripting/wrapper.hpp index 0d42ae3c24a..0b09a63c03d 100644 --- a/src/scripting/wrapper.hpp +++ b/src/scripting/wrapper.hpp @@ -24,6 +24,8 @@ class Candle; void create_squirrel_instance(HSQUIRRELVM v, scripting::Candle* object, bool setup_releasehook = false); class Clouds; void create_squirrel_instance(HSQUIRRELVM v, scripting::Clouds* object, bool setup_releasehook = false); +class CustomParticles; +void create_squirrel_instance(HSQUIRRELVM v, scripting::CustomParticles* object, bool setup_releasehook = false); class Decal; void create_squirrel_instance(HSQUIRRELVM v, scripting::Decal* object, bool setup_releasehook = false); class Dispenser; diff --git a/src/scripting/wrapper.interface.hpp b/src/scripting/wrapper.interface.hpp index 17a5c637030..b8735fe2817 100644 --- a/src/scripting/wrapper.interface.hpp +++ b/src/scripting/wrapper.interface.hpp @@ -6,6 +6,7 @@ #include "scripting/camera.hpp" #include "scripting/candle.hpp" #include "scripting/clouds.hpp" +#include "scripting/custom_particles.hpp" #include "scripting/decal.hpp" #include "scripting/dispenser.hpp" #include "scripting/display_effect.hpp" diff --git a/src/supertux/game_object_factory.cpp b/src/supertux/game_object_factory.cpp index 22f4f8854c7..ecd1ace16af 100644 --- a/src/supertux/game_object_factory.cpp +++ b/src/supertux/game_object_factory.cpp @@ -79,6 +79,7 @@ #include "object/candle.hpp" #include "object/circleplatform.hpp" #include "object/cloud_particle_system.hpp" +#include "object/custom_particle_system.hpp" #include "object/coin.hpp" #include "object/decal.hpp" #include "object/explosion.hpp" @@ -96,6 +97,7 @@ #include "object/level_time.hpp" #include "object/magicblock.hpp" #include "object/path_gameobject.hpp" +#include "object/particle_zone.hpp" #include "object/platform.hpp" #include "object/pneumatic_platform.hpp" #include "object/powerup.hpp" @@ -212,6 +214,7 @@ GameObjectFactory::init_factories() add_factory("candle"); add_factory("circleplatform"); add_factory("particles-clouds"); + add_factory("particles-custom"); add_factory("coin"); add_factory("decal"); add_factory("explosion"); @@ -229,6 +232,7 @@ GameObjectFactory::init_factories() add_factory("lantern"); add_factory("leveltime"); add_factory("magicblock"); + add_factory("particle-zone"); add_factory("platform"); add_factory("pneumatic-platform"); add_factory("powerup"); diff --git a/src/supertux/sector_parser.cpp b/src/supertux/sector_parser.cpp index 6d0ca2f2fc9..8e42a11af5e 100644 --- a/src/supertux/sector_parser.cpp +++ b/src/supertux/sector_parser.cpp @@ -27,6 +27,7 @@ #include "object/background.hpp" #include "object/camera.hpp" #include "object/cloud_particle_system.hpp" +#include "object/custom_particle_system.hpp" #include "object/gradient.hpp" #include "object/music_object.hpp" #include "object/rain_particle_system.hpp" @@ -207,6 +208,8 @@ SectorParser::parse_old_format(const ReaderMapping& reader) m_sector.add(); else if (particlesystem == "rain") m_sector.add(); + else if (particlesystem == "custom-particles") + m_sector.add(); Vector startpos(100, 170); reader.get("start_pos_x", startpos.x); From 3cced408235d1ef93bad3301b57048a4fc47c03c Mon Sep 17 00:00:00 2001 From: Semphris Date: Tue, 13 Oct 2020 18:22:09 -0400 Subject: [PATCH 3/9] Particle Editor, first version. There is waaaay too much I've done in this. I apologize to anyone having to do maintenance in this. That being said, there is still a lot to be done, much more than I can list here. (scripting, some custom particles features, etc.) --- data/AUTHORS | 1 + data/fonts/Roboto-Regular.ttf | Bin 0 -> 145348 bytes data/images/engine/editor/objects.stoi | 12 + data/images/engine/editor/sparkle-file.png | Bin 0 -> 5074 bytes src/editor/editor.cpp | 10 +- src/editor/editor.hpp | 1 + src/editor/object_menu.cpp | 5 + src/editor/object_menu.hpp | 3 +- src/editor/object_option.cpp | 35 + src/editor/object_option.hpp | 31 + src/editor/object_settings.cpp | 12 + src/editor/object_settings.hpp | 4 + src/editor/particle_editor.cpp | 701 ++++++++++++++++++ src/editor/particle_editor.hpp | 120 +++ src/editor/particle_settings_widget.cpp | 150 ++++ src/editor/particle_settings_widget.hpp | 80 ++ src/gui/menu_filesystem.cpp | 8 +- src/gui/menu_filesystem.hpp | 3 +- src/interface/control.cpp | 26 + src/interface/control.hpp | 66 ++ src/interface/control_button.cpp | 113 +++ src/interface/control_button.hpp | 44 ++ src/interface/control_checkbox.cpp | 96 +++ src/interface/control_checkbox.hpp | 46 ++ src/interface/control_enum.cpp | 23 + src/interface/control_enum.hpp | 296 ++++++++ src/interface/control_scrollbar.cpp | 151 ++++ src/interface/control_scrollbar.hpp | 78 ++ src/interface/control_textbox.cpp | 457 ++++++++++++ src/interface/control_textbox.hpp | 162 ++++ src/interface/control_textbox_float.cpp | 95 +++ src/interface/control_textbox_float.hpp | 66 ++ src/interface/control_textbox_int.cpp | 87 +++ src/interface/control_textbox_int.hpp | 66 ++ src/interface/label.cpp | 98 +++ src/interface/label.hpp | 60 ++ src/object/custom_particle_system.cpp | 76 +- src/object/custom_particle_system.hpp | 175 ++++- src/object/custom_particle_system_file.cpp | 72 ++ src/object/custom_particle_system_file.hpp | 59 ++ src/object/particle_zone.cpp | 26 +- src/object/particle_zone.hpp | 24 +- src/physfs/ifile_streambuf.cpp | 2 +- src/physfs/ofile_streambuf.cpp | 2 +- src/scripting/custom_particles.cpp | 6 + src/scripting/custom_particles.hpp | 3 + src/scripting/wrapper.cpp | 36 + src/supertux/colorscheme.cpp | 1 + src/supertux/error_handler.cpp | 77 ++ src/supertux/error_handler.hpp | 45 ++ src/supertux/game_object_factory.cpp | 2 + src/supertux/main.cpp | 4 + src/supertux/menu/menu_storage.cpp | 13 + src/supertux/menu/menu_storage.hpp | 5 +- src/supertux/menu/particle_editor_menu.cpp | 136 ++++ src/supertux/menu/particle_editor_menu.hpp | 49 ++ src/supertux/menu/particle_editor_open.cpp | 74 ++ src/supertux/menu/particle_editor_open.hpp | 48 ++ src/supertux/menu/particle_editor_save_as.cpp | 78 ++ src/supertux/menu/particle_editor_save_as.hpp | 47 ++ src/supertux/resources.cpp | 4 + src/supertux/resources.hpp | 3 + src/supertux/screen_manager.cpp | 5 + 63 files changed, 4239 insertions(+), 39 deletions(-) create mode 100644 data/fonts/Roboto-Regular.ttf create mode 100644 data/images/engine/editor/sparkle-file.png create mode 100644 src/editor/particle_editor.cpp create mode 100644 src/editor/particle_editor.hpp create mode 100644 src/editor/particle_settings_widget.cpp create mode 100644 src/editor/particle_settings_widget.hpp create mode 100644 src/interface/control.cpp create mode 100644 src/interface/control.hpp create mode 100644 src/interface/control_button.cpp create mode 100644 src/interface/control_button.hpp create mode 100644 src/interface/control_checkbox.cpp create mode 100644 src/interface/control_checkbox.hpp create mode 100644 src/interface/control_enum.cpp create mode 100644 src/interface/control_enum.hpp create mode 100644 src/interface/control_scrollbar.cpp create mode 100644 src/interface/control_scrollbar.hpp create mode 100644 src/interface/control_textbox.cpp create mode 100644 src/interface/control_textbox.hpp create mode 100644 src/interface/control_textbox_float.cpp create mode 100644 src/interface/control_textbox_float.hpp create mode 100644 src/interface/control_textbox_int.cpp create mode 100644 src/interface/control_textbox_int.hpp create mode 100644 src/interface/label.cpp create mode 100644 src/interface/label.hpp create mode 100644 src/object/custom_particle_system_file.cpp create mode 100644 src/object/custom_particle_system_file.hpp create mode 100644 src/supertux/error_handler.cpp create mode 100644 src/supertux/error_handler.hpp create mode 100644 src/supertux/menu/particle_editor_menu.cpp create mode 100644 src/supertux/menu/particle_editor_menu.hpp create mode 100644 src/supertux/menu/particle_editor_open.cpp create mode 100644 src/supertux/menu/particle_editor_open.hpp create mode 100644 src/supertux/menu/particle_editor_save_as.cpp create mode 100644 src/supertux/menu/particle_editor_save_as.hpp diff --git a/data/AUTHORS b/data/AUTHORS index 100b9ee430a..206ff7ee206 100644 --- a/data/AUTHORS +++ b/data/AUTHORS @@ -9,6 +9,7 @@ Most images were created either by grumbel or gwater. Check the log for details. * images/background/dawn_hill_para_blur.png - Daniel W, Romero Nickerson, dual-licensed: GPL version 2 or later and CC-BY-SA * images/background/ghostforest_grave.png - jkjkke - https://opengameart.org/content/background-6, edited, licensed under CC-BY 3.0 * images/objects/explosion/explosion*.png - Bleed - http://remusprites.carbonmade.com/ licensed under CC-BY 3.0, see https://opengameart.org/content/simple-explosion-bleeds-game-art +* images/engine/editor/sparkle-file.png - Semphris, licensed CC-BY 4.0 - CONTAINS WORK BY FortAwesome/FontAwesome (the file logo), licensed under CC-BY 4.0 == Levels == diff --git a/data/fonts/Roboto-Regular.ttf b/data/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3e6e2e76134cd6040a49ccf9961d302f7d139fcd GIT binary patch literal 145348 zcmd44cR*Cf`ae8#&e^koC`-piFG^da+9HT(upxHry4E+&?;ovW@UOc`Rj(bHR>dx zV<%3ZIlJ|n3Ap})5MA_y$s>mK(!6_!5PuyZPJR=I&72}P7uVrA;3t8ThD{u`vCaOm zgr1xZ_!do>{KoWj%@2u$p27PeC#OssHKmNclTApY47{_AFx=o+GT@44h+FGlNqr~W zAmsY%r_d%LH@&VmtBJ1t)On!O3|u-Bf!)Oq?>G&w{S>!+YNAc4P78TW=4U*W>aaUV zHxVB)glH_!>k(8TY$YPDMQIydgC|5&s$o40Fz4dOWHf6=ZHkclM0lD_0tet`AglcK z$&<$*t81z?!{9VrhzcZhM>p1DON{}g0*OFtSy|MJoE<_#a#+|#vZWf*L0Uj2X}%z1 zah^uUkZfT;DHM(qS)5K9Nkt@!t|xIqGZG?%l3ijCqLUVqsd(oMj)6Fa;7Gx-8AlrRr9r%Mls zmvog36Pl3KVit)&UPsI!uZeqzBrG63q|qc-DkiI?RJ>nIhD)u;2;l?L9Oon1eL*5S z1P{|WsR3ET&SeET zGDmnx3Z#x?ggBB+6(^8fVHH_~wzr5sl2oA)*@gON2r*=cxR1X-Np3|>$wzSfN@k0{ zl4%_84EL`b+J&Yc*jVs!U}Ir9gm$K{*;pX|3CD+;5hT(&4s0y!4k?r5@M|^}b_XAe z(`1iSNtScGGu*#&$nTSud@L{yY%J765KX5Ak!(SJ9mm^}kz}I3>Nv2mvmGU%GxPNd z@X-&ppU5Am57o59FU<3}Qit^^lh{e8@+^A3kIU;}M~bse}OBfI!Bj4$$GY+UVg?Ex}Y+XnZ+ zgNzR>`FKk{it{CS<8O2DEaw}R{{a5s*QP%h-JqQn|7*_po%0;ycQ(!zzGuA0_}@O~ zJbljet!56HAZ-Hd8_`xdS;6sVp$o5*I?=lh8t z@SUOQU|N8Fzb5Q7-QfHQ`6x{X-R&p-oX*f6mG>A=vN6}RCCyaXC~Y8f7~Gn7aUU`g z*BuyM8BRc7y_j4?JAE~M$Q01kha6W+K+D~5FqyZLR3cv~v3qC9Tap%XjPVnDt|xD> zGSW)O7gmnZSuvNFjIUxPPtu2%tt8!exh!7iFv$GwxL&C_M0!dy$Y9M*GC;mTCZLaZ z*nR#Ea&}Mp2{hK$^qrhZvQ+saQ__^COAkeO~6jWpxTI);~L?;W<`&>nwW765-5E$J<%pkCllZbD9I z7Law?V$^jF^|}%t`8U+zg0{wy6WSn*OAzD&lW!BbY+56B2j1=hN01Y8An}Gi%;7j< z_-q3_PJ~?ggX2?`Yb-bGCO^nDCbw)o(^X8bL5?!r2DsZoFR->eIUKA_#zV-p8uFIxN9Jl=OgE%{ zrZ2TIY6GkP-TCdGL+nd(%6`xdzZ{mR;#qm_}@N4w1-k2^!&3S2AKnbeI& zJ2fJ$w0B5XHhv6N6-GYRE6|6LWDl&s)zWlRjo97vv-AzhIiXJ&dyfC}kjWSWE;E6D zz_dpii@b&DS1H!?k-Ubq)2t$G)%vxUFun$|+9?6!{Q~X%2s-_YOi*E*XL)c8`P50zvbIxd7g_XlL6tICtIi09| zR&z_8Tn2!KxLkNe$Eh++)m56cq@R>drmMNU2|V#W>=W<>cs&PtXdFA#{#$c<9l*wv zU9%i?&uEYF38&Rpa|?`&4;d{oe&IBGp0pJ^f_E)=vgTSc0OLAb-V0grPvm`J(?z5I zonfy!gGMfr_HrI+>m-tc@@|at@1PZb(8@6q3f}7gc(yT_WyP5)+hId8J5JRZujc<@ z6FA_}wCBG+{|lSK;eE)1|HD?0j+%auTEWKon{r(CFc9Uhe^nP-^rfmd|1RhBuj=Bz$TibVp^_KoOieLEvE;&O%t>4@HyMa~`8bcm`EE>D zph?*e@}yjzODfnQ?k5_ii}|sO=TX8F%v+w}xD6T^2fbH9`U>xo<>EEy*^y+IcCTe_ zgE@~d2Kr|V=sX#^d>?5g9wo1fL&EtbvwWV6*Y+Wkv`feo$iMNhEi*OS$N|` z`br^(nhH+1$L3a-Npr}C=Gr3Cob}NukqmH3A_F0xw}@THSh0{y5hq}N5C^^-3;E)M z`N2O)KVdNB`z+i`Lp}_)!E%x!3nW!~k3;~*M+vTHf!?%ar?{6akj{_@@ZU_y#nc<` zO_E&5BzB)4`^W;imJG#Q#Z!<>=R{Z2PEku*VJrtohsktl2XM24?8mWAqJa5RG7sm& zS^a3Uzc?53mAPb^xSsSypL4|lWGZZ{t?FDxYJxt+VP13=W95$d4eT%Jkm)ymu>LaG z{_bG?V12SXfa7;9^NMRY-^*~raP;cI@M(W=eA~Cf>b5@^-t5}3o-^FqAHX---Noug z{xM)_jbk+%4>m67=Xf?o>cQYs4+j|87^?>xbNhqg1&2NS?A+-g$}!qe4@N^c7!9%K z%u^v!QKVGE|50BWK^xOoev!D2rBoYixgSh}S^1EF5Esjx0P0R@l=%Wr*sq>BYaljY z^?On`O67oNxEbI^qx3Y4(l9j3eu6n;O{l+M_H*Oyxwz5*{$ov;e}3wTng>w)#k!mS zaMM$LKmn=w&+#tRvl1S*VwSTwOX<*X^9>F9$^OurhH8(@PXee5{w%*nJP%;4S$aK7tnB%3s$u6PK1_=m6xlE>Vvs74?=&OaHA! zEltw;{0{KQ$x0uZ;1S#aq!AUI89X$2RM@rOK6oi9CR44+1i0#wK(Z6? z?4-v5&kMni!&3!#?f@QQg{KhktP=N#9|ImDE%PV-ZTt)TcLE*~=o#o67#J7^c;W+F zSmD_Ncn&+jGr$6myB$0uIXo2b)R;`BUrmop7flA!9@9osUsIMT*;HTIpcL00Q06LQ zlmSYArMHr;yjJ^F?cLfhYrm+yQ~PP{k=nzL&p%%DxZUHXk8VE-ebo3-qel%N)qkXW zo`m#GQCxmBEX6!`tpl{78M$fHVZHHYWZg02C2Kf=LJoC1E6-M35$=DTyReB$~vK zSfVF!q#21P2_%sukz@k?8ZhL2{TJB1gzka-19^22w`eA(-vKuXKvMODf2F35f#eNws5p#_Av46jVm~p5mWqSO1TvQ{A_vGBvVe?$^)`T} zi-YN8GLg&^Q^|0$kSr#fAfdd)II)@7Tx=mGib>?OIGnssTZ$dTu3|eeLmVlN5?hm5 zVzSs$Oe2M)m@FbINfB91){wQ31sljl^l&}dM&2S@$e+L!h; zkm$|^=j@y^N>}weMNNwhg^kOc#UX=Z3^Y10u>H6U105P;5TXqGBZIo)#%1sM$uX&L<*1_cGSH(bchFWHk7@VU!_snSz5Q6}sOSHkKeT<<_w80md zYjjxzT^3l>Gv{;~IA_EuC$g+p&S~N!KAF<5p&6;iPkfzd2DC{H+E0c zJv}kZ2WP}6J84ma8F9vHC;Eml(1}hr#<8;kmP}#E5Emr*EYZ8*X*ZTkX31)n9A^oh zzG*bEvjBFM%90_Lvp0-xuB-x(R;C)kwB8o2G6}su7fKH z@m7Yy-`EnqPy_OnL?;H3I&u;T(*{F2I{u;|!K!FHy(nY}!-Znur0|XCDu#-!#XjOB zu|j+zwUJIqie|8;N>d}}$zRLAXkE0i+CJLZ+Uri!oGv&AI!|*x>8!ZqxSXjIS|_c} zv^uxy{N_5))mT@m+o|r`^_=QWs#jX?wwumvzS~WmPS-&1R)OdFByx=8x#d*#4y6E+Vcc%9$?>j!pJ|#ZYzMXtaeZThe@+sTaA=#* z1EJ?aZ-@RECWSQ!iw_$bb~o&oaM$qQ@HXLn!ncS25#bUM8j%(;IAUSMohD0~lr-7f zq@u~)rcOA0u5O{Ue)4_KKVqxiPXjDl=+q)Uv3ZQB_gjM5jlOjh-1@ z7+o5DG5Tio7tzmSTw{V`+QjsVnG{nPvpeQg%>CF-u~o5mVjsu8&nYzg7n(O}9^X8xdH?34o6l`t+WbiKQ_XKS zf7~Lx#rhV9T2!^T)8cW97cGNZ_G-De<%O32XnDWo53Ph&4O+#u>dzCz!YeP{N)mJ^zjn{&P2z<%ZZD*DIwH}tFz}l} zO$IF)^y8r428R!h9XxaJ-oZzPBo1jkWXcfZkROM}4J{aYXIO(_Ylb%%K52Nt@FOFd zjK~~uZN#k+UyKw+x{RDY^3JH1qxz4UJ!f|&~n7HnK_ZlTY@u?wd!ELbEgTDWM{ zqS8fgFETDVx9HlUTZ^6)>I$O^vkP+yhZg1)ZY!)=?6G*{;*!P37T;d{d`Y7v)0b2& zxwGW)QnECBY5LO9OD8SOTUxU8{?eLdfy?5TwOQ7GS;4Yx%M8n|FMGaBS?;nta(SQS zqnGC`U$}hL@+U=UMGK3HiXN^QzT#GKTJiScnw2A0UR%|F)i*Iy0uN#X09E&wqWhHwU^dbuf4nWd5KiwUy@joRWiI}S;@ANizT;9 zo~_fZYqGA*x<2c2*DYLEy3V-n=DH{Az19b=k6xd?zSsKP^+oIVu0OT@*7|QYxNZpC z&|yRW4bwMl+;C*WxeeDg+}`kT!;c%KjXoRWHfC%bym8vb;*C2up4oVFEFWLO|=8DZXH$UD&wzzKb+Y+}W zd&|HrW4FxTQnY2~mWnM`{`T+Imb+WN*`kzsl!lkKEbUO*qjYfT{L*t<1GlDa?Xq>? zw(xDS+mg3sZ5z97`nF};s@@Xbs`u9R?ZMk)w;$Qje8=t`#+@EJr|sOl^W4tIyIgh+ z+*P>i+^*ZZb-UN>{>Prdd$#Ynvgi5U=)Ilx9@~3nZ`I!0dvCv8v0vElwZHlP0|#6W zOgT_~F!A7$gT{k3hmsF1IduDQ%fr(SA3OZTkq$?W9hHyvIlBAkFUMSuO*&R`?8Wiu zmkDLl%8JVF8k-nL8+X6s@lMe@_se^fUn~FOgmfbIM4uB2Puw}# z=Va;07w-;#cjGDDsk~FCPB%C`|MdMc!Dn`!dH!DJd(+-KQ{i7xT5-KXIV+!yKHKZ; z$g@RfE6(0N``fv=b2;axpDQ@`_PN{Vo}G_8pM8G*`O@>Z&;NEI^g{N9*%vlnsJ`(0 z{lNEIzCY#t-R~RUzxMv^i^9d|i$gCKUEF=qc=5``=NFX^d_G9~p#KM}J}CX5{DXg7 zBA5Iwg^(@ZPSlm zNVt&XHvNoL65Ksnl*y98B`PQAPQzdN#WkZL?g^eDNeOgHWhJeuqF;-*USwZ~!6Cpu zuM;|BtxtnFn=>&;dV@Zd$^@drha7kj@0{FA!@ zS3}I@mJvf8y`iz51LO*TTvh0FxX`H=9BzQhi#5QL2JE7-u8e4`FdL+5+%d@2hB~@3 zC%gM~bcTBDrop4y;G{En@nSyJ2BI_g@jLzu{17n&{e^o1M}nBZ4(||tAoUCpu9&hn zW&c2(GE9hW>#?ba3CHD!8DIXMy?LD}!$eD!(X_Of4qQcdDnr?^O4(bij26PNB0|X| zQ=H^2zlAw!FY`z^qZ7_*_kwW|%toSquro%&P+w;ds}0UNgDXqRJVje4gLQ_0YD2KD zEYfxp&?kmRgoh_3CZ{ANc>DNxha`rF1k2uDKEAl{lC|C;N#WrsDG6Ra3GvBdc0tLt zn`htNe&F<_`BU=VoVEQ%)y?v^j@*@mb6ck_SW9R2D~NyX`k{T*-d}y~_w?$r19Qd? zo0*(mdGN@Go)^x0d{(U~T{MS{rG|_(eXm)hsl?2^A!gwzm}Tb?Lvy{MrFld}bWBux z8IFr^Hg2NM;KF)zr{UdxW$x70IZ;>UXLlKnzN+O6;kvRIyJrEqvP9cuTr!I6TSR(WE3Z8t8v{riq}wWA`p!zIV^EqJ1UZyL8O%-l=o8px?WE*}lC?Ew_4f z?9^Rxn)Mb8auYr z@9m^%?ZA0yrthU{;3o(p-vaYBzv7aj@`bB1O8)Z|I0j*ZKB_|iPXgVyb zdk$ST``v*fw)Qyq?#Y7Tt2<{aW7=-dDZJnBo@R9G)Ni^pi>2>0&X^lNwM2ZF^hU;z z@P5g!4W7#AhC+q}P#-QsWF|pC!C*f~8!k9BEtGGlGu%m(6e`Vxx8#xV2tm?_dP7|l z_0*9RUtd{p_ttr!SK-9HkhVE4hcEx|T2Z)sT)8N8qeVjOAUb`#(nQ%;SJ|gDnLc5V z5JOk+wq?{Apw?Mek807pjsQQ&9_~pxAtEKghqwy?%KOLU@TE6CSr9HCqp3m%<;~hp z22B`8AJ9Q{X&?G%(u6^^x0F0yXCq;V*cURb9(+{*(k5RS@z>QE>M#)#mZA|8#4ult zr&bgrXohm98ExXS%Y}x;DYxEVbiz<5-tJdAnf6ikPTJN_c|Mc|DBlj^^=FY1DN#BJ ziQAAEoKZg?dD$idATZEEkav)Kh&x1>dxEf!u2!a2DwAkQrRZioK<%K&JlSz{9_s>3i@#pQ&{;XWSaN)9|g$tJoQOdW6$D01u)s8h9T$A!q=ZMukj8hC!n6x@Dnd6nMtcRn|4d9*R^}3^8_gCvJt8c5yDAXqq6-JS`Gl}7@D|5Cz z#3j=&5XQbBouHs3CMnDa2#E+M7WqMagQW19Z2DEffc$ZrR-Y9#RQl%9w=46N1%gnq zRPb1RAZOx+t;KQ$CI2j&@pQQ|ghfXWF?}z-1gw!{hIKsM0Ir(~u2s@~MCc%604x>b zd6qn7m-#HwBQdz?%CvSMyFqhQ8ye~ifh;Wxv3o?I5^a|lS!g<2cerwZ6lCg9f)G@7 zAuAC=3y&mLapIzh%F7QgD=#%-#mYJJR?S~_L`+!p=DdYVr^x&M z_1WeA@93jWO}qT~vs3aL%a$!(TEJ;C1>@5LW-4|N_NvKcpt>@_1}!sM zC=(J)=hv%Fa~@xBPQ4ZNw$_sdv5t6$aHggSG{`+dD=xo!^-}FLVPSP`0j-cd>35}y zfo4w@f2wII9H#M{RyKn_JON7peAfmiGb9xFrz-yI;i4X65I`c@7}`)zXYi~>TIOR1 zo|CSukzE-WQ2`(sPfv&&F*!LU*~8llY-BYs%xKph?B}10!SCguK!MVDA75=aH<7%PK^kWG!6Z8384PR5 z%>Rku!k*6S;v$=k&)jxCZQqa&p8S6Ew(^8F-#cnm*r9@1OV?^DgBxtBAMoO;Z}U{; z`9}|xM>H9Wc|KSq99TQKM@HV&FK%pJa|Zm-mGiCvYaDuWZ|a}}b=2Ni$pV(EJ%?lu)?!wuYJrGmd15v@?a!YVOq{<-`SaFo<>mVM6X*W>g9|@z+dgZ~md*3$lr~*D zX57L>xnm2Z#A$~kqbufI`}EUmb1I@E4^O-B@y9i#GfPWn&Rw}mY&>Dr@~M-TEMfH0 z6Md!@ddV{PxGs8JSM9M%FJ;6Awo&U=7wG zKAte8EHcK+hnm(LZMLreCx1|F#bc^f9{scHj#U3v`O_w@4P3W!!seBWzxXWu^^R2Y z&o40_dZc_0yX28l-PnIqKz&*}xa0ium)_sB5z2#L<&*1B#5cjXfqK-k6cIDf*K8Ii zqi3>98|Z6Zb_DeKB$SOnDJo8&GI7HO6iZpIL@i}Ohp!_#XNLFy))ed%bX>MW2bHxz z2L@B??W^&oNz|KW_UkiL@7|*S2g;HS%Hm5uy2}Ve6R^%l#5_(1{#fbzPc5mcowCL@Q^ZIbTh*fj zz)ZCLg${w~%xA_UXx!)#y{3*EP*5R?-y1MW+)o6us` z+DupGZ=3!YqI6}Uc9+naZEcr8z0AZjH~+&SNub(&wF#o^0~ML#L4pE_D3BnW`-02^ zi6_5B&5y&AQ#`q(lrK4d{Z+?Y-}p5{=M{PQ{&U~Bg3gkh;QU&`ob^sR<$_{Rt}Dyk zJZLs;nRB8|c)O2AD22)}4^MiXPN#F|hLrYTupI^U-lSpB`4?m6ggerE^rs_((nd8PN#)1rNowwszI}W~r_XbwUHz zdS3#U7<5eo=s7JcW6SKy!P;_g9B9EeS|;|Wpka(p_2sMS4k`>PbF#4kv&&qn%Uij0 zb`b`hoa3^>Wvh!g$T-f0GF==b!W2c23Ucvk?e?OpLc&2I@@j2Q`rR{`&QO+X3@@^U zM#3g=Mmc2uallp&k~omrY<_>ChgEaX_|y;Pj-Rf~%?=cBl+NxFsye`S8P_bqJt$sc zlRUFkLvhK;HO!I+mD4XDQ(^9?!c9u;*UDkqd&$Om3zbzgbyCrWV&yxHp|Wz=masJofahJ&dJLhYZ`;HOZo0HrT}|1-Y*wZm+GrGDPmrM zD+b4Gz)=TL0?eaj71OE$QdPY1X&q+ZvP6%B`Ks(x62qQ@T?kK>J(w^6*%Xf-l2`30 zk=C>n->Y>IwhDV{e=pS-wkn9pwZJoj;{_fzi~YwzP~lM-!`P(ETwfD`O*Tuv=N z@AQ$AFx8mqM5h@iv*amzw1Xwx*z>1OCMRK#@i=>H2ut)x;9GmOD|Xt2@eZ?M8T68S z?bu53gIYn@EELwxWl*~S=9!?kPe5;hh`Hbh{JR;Q!L6Fj>pX0jVe>lsc~5}o!WcY2 zU*>Nsgz2p;LB26FFCkuHt^;lYnx}gyOgBlNBr2xs%1iJ{o0l)BzbI5VAK$nC`eVb~ z?ZL|Xg7kj&5}JjGv1lAwzg?8`_rAEG+`9NOJ(70bYQ{Sv3mF~kwc?$tYZdPfeX>#~ zBNr=$UptDSZ@3DeBu^uG>~oo2eVynOV*KR^9YUb=W4Jdm94G=XTN z9wJ|07x88s=-df1-$`v#C3;>=odFp?k{R*KAnx-lNtHqHt6B1tUG88B&cL)R(IYX) z4wP=JBeO{dD4fmH65s?+Kn$feLsGMjt4BaZJsaqZxSRT0mNadK4c z4L(6<7cM#jtu9Zb8PJU<-=K@=Iw}srK*C>;<~-Ro-*vU?4p(WAv70L}G7w35lxr(j z0V04+SfC&ifC$eB=t-?7&Y-=kWm;WR>7`McEK(vho|PELVbTt$$}}OHzML$rqP6eT zUO)#Ncxa}%FaM7WgyAs=)yB>O0}ctM)508K1!N>ZZh;%DCr;JIWbj#9-3+H*P9vSB zI~6*ucajGgdpR>b9|XwRSW+x$FurIfgE7v^mC|V8@#)JQgJh0Tlu56KvIy8bLXs{9#hwr>Q3DL_1gBIeFy9jU(@VCiGE$ zRd&*$v<@_+3mvZPP<|DzP+}ril$X~`#PlJDk?E%EK&=aOWV#un4g|p!CNqmLdtg$N z=5f^m zc3be}r)dRzdBP)ZvCkbAdeoU|nZFG?M$y@y!KKrc`P%LR5bo8cY-(nf>3@N_k^O&P zs{GJ=R@z^pZ`Jc&-!4Fev+w_e!+&dz!}k_~m_W`te~Cl>!~-(N!x5o&-Uy=$;Zthh z)y}K0;*~q?Vb8I(8*eBmzJ}p|XSb&{Z%&yx&OhwK2%n(Kdc zo+;pTrwYh2?kBC z0!?dGe~N>QuuhyHs5Le)A%=ivi-%lVr9@XMG13H$_lxf}-te3-*|QEfy36IL#dqtV zvmBI(s;^&BVD8+%m;^mw_9D4Uca;zSe5QO_Sy8(Cvc^yo_G3-$zr`7)l|++!AOz+LMl;W=XAO^Az8N6h2Q$5Jm>c zL3HM)wT}+gKDteF-*`jn@FE*6QFy0`{1MK)5dr$2Z8{h#+o$=ae_pBVGAIuk$n}-G24(jp`o~H7u6U{@ z^S$@PQ!lc`=ZhBA)Z=|i2R?7(tPWyzIh+o&0*Ah-L7XWxuZ^t@ z7Se0a3117h7@c_mzMFv0NnHEMl)071uu*rg;tEf=&=>p9^|D3;7dkS?$V6CVv;1;1pupY%hSJ@O^A`nl+8DZ!d zjp5Hvc-wW9D|97qI~4t7>wRUColtP}Afo~_ngj!;4qd3sKYLa#_#<4Yf^2_zmqsn2ktg2P$vWY4;j`MNbWaBaJuo~-rqSXKoLSe# zR8eJ*i@0VQ<|o@xGfLcA$`;iqzIXB-{SLBQLYzAKU>w5e@;FQQ(#d4DbkdC_CU%dn zpQN%gHzWq@GL!MRLj_2EV2-L?uKh6C07H@eF8nUZ$Zm!Eh2h}nYm9W;i~tG+9H4v8FOQ{s>sW#P85 zfZ-KxaZ|yIoRDDgtztO}PI@2s419dyjZH~1$2N#fTl~gn?&#*N$HxzuJS;=byY!x+_V<<(64y+6-7vFzW-s2~ z)sXp7Sc`VYYDqRRxR@!5PY={CtE0sG&!sWblzRAt*iwnNtq06nYGk`)MhbVmY1zbp zFEzNEV>7hwKFs}=Bt<=_9KSu$Z&zi)rcqb!S1vCeFfS~8&fpcx=r7+X4|;ZAE&J8( z&g=pQ+siZG_v@@gW%sYL@Gr*cH;hwb>~P?Kar<2bS%uP`u-I~%uRV0kx1HeY0}U;} z&|r>1i-SgH28i(b@C{{xMyWmW!;6>SpZiG?jK$-(E-qX=D@QK=oF;x7`7fGue~&z3 z#l78;Z-2#TXEA&_MTC%{l9VP$yRB7e1S=GD1%^$^GIJ_qG~m0vmD!o} z&vxCud(TYsBLMfmflD9Iz)&+bxe!)?wvu@r!h|qfJmuAO?$?16WGD0DyyB>XXtP5z z^YJ*)geI5(+wehnczXxIdB@zaaJ`CHdh!hd%?sHNm0zi6#h3SM(?xOf@{yZTy0$#G zZ$z<9pg+&`rZLZ=|3wp&Kkm-aY`fYbzMw@H@yh=Bng-LEPmF>Zh;S7P<-T@ne-0zy z32yd$wP%cSsf{+QE**P+B-SXss|`Mob{K*ruqR_n>q;71r+jem;ECPA?IZm>5*qdC z_27Ycpk(>cvvpiHO7fWD%S&o1z=2#(>hDJWVI%Zp<-&@ZgXPsytpR}>#nng+L zhhBbMS+ug>%!u&WeO4Bmsi|ky%9j_a??r`e@efaKX@()v*)!Fr(mKH^+V% z3v|#}fiMZnJZ;M{Y$n8gKVs!NEA85J(ovf*gwK5Zts5N z130Ubhgp1|7F1Twr%irfoiTedwO-Pm2@|$PBpjHs??UCIiKFJ#shXPGe|iAMV^!DQBS#LL@aS>vEMfW1 z#l_>DYP$=|H?Qo}uI3(gu<*WdzV}1~C=IdyvPMG*jNSDzs_ZrAn}_OG7VF0SVJnY%UxLJTt$IMSD8PAk{DCFzE$n zIs^}S^7UY7O18l9O3dLfD&2cPHA>C>`^wA8wX0W_tXs8mtq}2yrYP5b@_DXYr%68@ zxqttN&!LA84{?}QD?_EGfQi{5z0DYMFhU$gpUVzaE^^QrhWwIH3 z*Zay)@jDCbmgpo0eDVQRiIsDs3cE_V|J91JN$?PN^HRK{)Q09Cu`#jn#>&K11EBer z7I%LmBI1p1E0>vNb?40d7vX~ZS{tVMKg?_^1i#kdDKtN)N#PLSc5|2msep3jqh473 zrkLqkL*^_Ch+v-xXm2ZGeSC%0qq4>~t~7YYF3s_2QdcaN!3Qs;mfL#`)=C&|v^@qk z$5pf)LFSuB+d)db;*TkB>f6E>-q_Q=SA9SC#gvcvXKegy4jeKzt_Pdn6$uO2aP-$0 zf~)ZXJnvvw=6~tDVbR0(_MaL%Y>w;U8dH=;6jffnUw-x;95pkBj~m^;=*E?AD?a*B z7AiC)!^h2_0d0E)M6Js&Jach{{QA^^ZPPk6j^32N#(1k(9yhEv`W$TfUdYv?!Zxu) z_3(MGieet5qFz|N(JVsAuBWhJo$M3};@1$cu41| zE7LJ4YjuM-YHzmWWCq0I4uU_~Jw$Aiuyb5lRpsaj_gg}t*x z*Oyg*BNkJ{$AH6G^(s5aN(XMTPRD33XWMxPiDcC4v6oE_?oYRhhOy znRtz+cIhI#d|Ab1A;qp~qPdH6Z|nm8Pr2+Mb+{i$MZjPCfrM)K)P-KcNMX8AD6ALY z{o$@5+|~Bdj?_-q7HZcspAZ~EZYbN?ZGhW&w>fSr+_t*OR+v34ZXhv2tL01Xdwu0b zceZ~XAU8gANvYcLaf}>wbB9uOi54wdBp7P5OG*U8%PL{rDf({hMAk7g4*!AEI@rt2 z`{h7u4*lZTRW+t+mLaNz`+zpEIW%|w80WAhpi(5DHXs(81^^sKm&r+!v0>q;O{6rZ8{X7a=lr`BG`objw1&PF`Me&pN5u4fV>TyL9a` zvDwSOiM#7)wo-XY##AYk;cPJI?hIgb^a-gWu3AnGbISkIC)81xTC-Q^SbI)PC@n1# zy|y7@i?x-itQU63uVGJrL-Sgxp2fmo6*h?odH89{n;|C(Z;sm;6T557jly@(OV;*r z&==5bhI#{o)zy}?IchKOa`ju~iF4xkxcX1uaF zbb>ebVK8{TCl2mE82=_Vy|{1GgbAbeUHmQdox&UAcSbecIq_!UiI6u_o*uqAbT%Hn;wY1B=DSgptgE5sc|&hDZ%~TFTU2+G;eNcS4nF-54jb3v z>vjkep zxJPT^T;k&7z1v;xS6pFRcjgZtG2Mmk7RGGozo%7cY28a>Hf*K!u7CUS5jk(#hQ-SB ztr*9#rhjVs<2#)!GBlPLyzOSC5PxM6c9aA>$Jj1aO%|brc;P#ZB2vuew{MCs4?tpD zOel-$@nFF-!GhGW>*-OWPIq3oq9{J8MRLiC)t!t(hZ#GU6mLvykrcP8xaN(YjvV^w zr$a}6n$RY_?Q4Za#a&K}7+sOIVMR&GYi*iuDBhBFdercDx~yIy%&Aa*r_L2;sFQ*( ze`8GiF(xdw$_HP*;H2n-X^xNTyLND=$W@3hPlpvw zxcT8H!(s;WmesQ}Joqj*$W(Z{*&mEOcxl-m-0WlM*!#+0+FV&c5gA{pEN8J`F!cvL z6zdC{YU8&q-Ku;^!;lH!0fDTA{OQBxj}~LW_z9C$D@(C{APmiFWx|C4yMbAkYZ)s& z7yS_Ydqw!o%$xlccWEx@GXdW-8NhdkG}9Y=&DDjlNg}Hb=HhF%(KPVwWeyf1A_^1j z3N)@Zgy_nmY~AP5#2P=JDyT8imVIZ8Cb0M?S-Fnek73-M%jC3=50j6H;nLipecN|H z%=cHyEm)E7FJ1oDn(I#=4(^+gL0#{EOC?FH*j~JLTaZm90|pEpJ$mqf{P?kK~BUh<~5K0P+$=1bOOSmv@}=QsQgJ= zQ3F1LRHl(dW`Kkk3Rj}RVs|vPDpy{tx6(at6RY4f#qZ5~V6W zCu&~nkV)yWF>x%0dpOAx$KyM;;bObrzD9&}JDOqo8nP>&&JZt(!HD_sU=&y;*w~Jd z8vBsap!wIB7JTJ@xJu}=VnYE7<-Og4U z?eB37WO1bqtB_5uRY8hHt%A+t#%x$WHh0saj!B8HXC@?dlrH3M-I_aL!-fednVBgm zojarb5kwMymwrb3Zp?zR>@!l;9E)_d4l-yV_y#elCs5$ua+4`;hwn5fi zVJ#i4l$I%r=)5x8vgRzl8I@D2jG%9o(ycGuw=%psB z8>KJ<)tA{o_QH;1*k0}BwIjbNdQaqcw|hCL?GZ4B!);`SyQ_gy&{`3L}I^G1GUFx!M5k<5+tyB%Zs{x zX`Zz>#y2whPof9fwe8rxdHdahd~p8S1FyH~(51X2|Ab8UbWY9e)v;rtG_^%yRLi&~ z^57XWCZ!B$;5#&9^4#f@<=7Uj;@Y&B53hG_2xbhSt-leJ=j$|;vQop_+5&&yb3T`~ z%&q^^eXC9y`D&Bu?Xx;{n2J=Ltjy4|vyp*1^WrFXmY8=fM8No=gM`T=htCSEEOU;| z@0mHV$t9^}?R?{c%noA2%c=>f=|Le%GyFd&3>T?T&>7%s7d2*;Pb*Y`sIKP0`R>kK zxA5ZzxeNKdzC~n`_${q3I8pKAkErHFHmhrwFolj0GobNxd=Ih}RR9!*0Uc9SB~1B; zc?@PKUFmV0y0JJ1)<>(K)=5P$0v`-+>IEP$8_yvToLnMOXsB!3KHJx6GjlTg1}4_& zkv(+U#AeOn!nI2oEJK7Xv>0Qo!8h)B{kGi>YL(1#w66FYtOAVz3wKx;Ek2e{V}vcj z>LtuA!S(YyVif9RzVI=x$TRCcePcBiyV;k!1{v?UFy?RpU*J<+(~NE`Id6G7%^1ke z2$C{)n+j&y2B8zS$?|{Ou0ONlt?d=>y|w-9(VRX#`}ObDD@V#(|K5A+-`sHe^oE>q zxqaX4HGVu>x`&RPL^g|y#1qi583>`$iWzhYzkdoot%tD9BE(~)pRgHu2kJ+hs8&2A zoWZMsuVN2FWu;BFz=^t99l!EAI?ymg>R9SR9Y@3{)M1H+=Gs-IEOj}FZOvlv%|?vX z4#-K?{ztryLn8j~A=Gi0=&)VS0!J|H!(g=zRb3-q3e^Z?nvy3Tm0Cg8`ZImwZkA?z zCli}{|1Ue4WZAsopIL=i@8>ond@0bs>vg>T=f`n>qfBtPtA z5_XnM8IU)>uXQI={-@KD`V6F7KW2SH43nmTHdqM9ml)X|wRW%<2f>TT>5{ujyt{X*L5tPsg|cb$Z6CM9%OvQ>-VtkfiuI>%UU*voS?(h zvV34)En8PDOLEBu)N2_{wO-x`2g>}S75-xNJ$wQngC4@SBaH@caDd($bnnKJe3lS) zZwgDY`B_6&gjrE;EXikyf!)hy2|>arMlk&VA|3|0#nx0mwZ@`w2ZKr;s_^0vs*X!| zUD)y@A}!LKSBpDDX1yTV8S;H9m-HvQi9^K6qBxZW;Mi>`F+OGN3T7T;!1|!w>KfC~ z_LE;Qu+BCaG!FJa#dDFMKKS2_b3Jk0g(dk|uQm?G*~u=R13f2rV((NjZk0!Rw#1AZ z@-9eZo2W2>X4{87gRqq7|k*l)r65N+;*so2&B zuE#h`h+a3t1Q(?+R?GTeE}us-Y<=Ja!pmcd%gdX7LixU0)(2jm_Aqs@^?}!KAky@L!3iGX7yo5~(qU!k;FQqY~IgF=Gl zXj4!&-aLk|p{A*_E)dc_kdF!ch2}9dmrt;k&!b<_Nox5$v^u}nTjvQHNn{Mi&11`#L&+hVc19^NayDGZ;(NnrLEg~!SlUIVrZBM71G5%`cc z)1Bt67`->84cM@vS3m5>7<@3Z`-Z}B_Gw~e|*qiK`U z?|S`EMENd_gxKX@^?JBKsqI~oNX7L{qp@A1^OAU4Gc`Ea^dabs$DnimMn9DQ%HZ*w!>{380`1vz|E+ZgY)=6%S+ie!#T&)mS_F!9Yc5Ha|EGLe|xb43|MOJPpS zK60Z<33a@!69&w2?R6CvyX|xpn?G1Ks4yq3r@Aecj-_>vES+pI+qn*r*Zf;y>bzRjpPWssOHd(%e;h0X@u?yALs&~8n2M#^NYa3{ zr&mL>>vAV5^r~Ihq(RSC01OHI^J6V-?;|)}{geRnI;%0-7H=wF5^@Y{wTsxMYJ{uw zRd99NV=0Xz$!vld?SIq7&V_cJ8UvJf5N1QI(QV6rEMtx5b?) z-8&5uV-GIY1k5f49Sl?2vsh`+0ao+4z1Wu9i#+aE^8oiVEj)w&ON;T$g+MFMn9F=+ zEt5yPv%t2^OM>>?9L#x?@QR8TFWM8Y<-4T0f`Fp+jyt1=A`-#$%X$(#!8nlUjuC1(>&Fn z!$a7)UX!W;B3KRO_ne-x{p+lva=lY3_DJ*kpl326*W+MQ1S0AM9u)Y@Egx%BgFYNS zbAwO8*l`5(iVsNJ9PP(K|6na(VL=nYR=Kd;kA+a?^TU@GE&CJ34H>K4|LhCpTk7>` z{~d{vbMFl0>#o?q(lfesw>7C>bXD9J9}L-*w`kX1jr&;T-+S)eN~@ z(U87xbcs)Jy}h34vGYd1lpY^j->Q4{hQg~D3&a+SwK9{f-7%Ij%^7Nzt-e!yxoq{_ z>1*)4;9Kp>WUDWb8i<4;f$;*1edKzghlS>kTl7RDE6tnBFg;-|lSju{^h5v~?SQ8N zrU0fV@PGK|11x%izr*zedk1ts&a5W_?*~2$WO^b{7syC85a6UCv5c`99^M!8dpRS` zeF>Nn!21%gGr$mVA)q>d^(BDy1&Kpn7)|qcNp6N&Pc&xK-&og}wbz)n$C78@>A!?R z#CLJZr5MQwDa^hkf?cw(WvzZbX3}a^lh!`g0aFo!)vs4p$F_0^Qh^^m4BkN&fhNV& zG!Va0rdHu6^sc!5VU6%1S{i1Wg&5S+j`l4#X;l%=ja{~I6Kj*d%#~)hZ`2sY?OS{S z23EN-z-;9P=nPGn;cX#OZqjnPRgGOlPaIGV{XU-rK5Q(u!Wf8RI2SMQtZ+s$`~?_}S6U&r^E{9*5d zKF64)j}J&A)elKNONJl;ae+_F?J?QI%ah!6v*dw4r~IDxQ*S0=*ty<2)tgBkyjXtE z@2MZV&(8IJseWAYpe`Q!#bxI}-uFCs6wR>LXRz0QTHjQk!(Kn2zP^5H{ciP#)Sp~G zzrI;|3IzUkDFG4UmOV-qdO}Y^8a+q|Q9u)l2m;cJiUcWA1QQUY6Y8}920^8SCV~Z2mNgGuVFN1{$Uggi&zYIsnTWqz{=fJ0c|U*e&9E~&JM)zDoafZFZJ8=h-O*XeUk!-9s^7(%De z8@eyyS%$rFnyW(Gi02{aEF`Du`|v&FUtFKoFP!-u|1OO7e*PV5(SIj4Tl%kM?=$%c z%h2w+#xp=8LrO;?Z^G+>gz5Fr0iS>lL7!6bse*)Hv~?9O>>b^%q}{@Huy^4b7U|Ic z1UWz-sC$feyTZh970w!5pDYJZ{JGgyTvvwkb;jHYBRc@>TG0LO6^BE1fTqyZo3h^2 z)Y9d^bJgE<;?t08`2H>?pN4R-_hj0+v_Km1>9mxzoHUb98}A_70lx$K-(l$ZrtN?& z(INehpU*s_?CbD$z~v$rK!RW7qMP;oyA!sNnMlyH1I_tv;k*z>&OgeQ0LY%jdB{`lC*nG6_!g}L9*)+alu z_f2ntHJXLp1ox_A)z49(78d)A9PdJs$x<@Vl^h6W-by+xz@eglMo|W&bO?dBvCQTz z!S~e5{~y}W&?k(M_6o+R0mie`g&0bTSs;e>M^;A4HGN|RoqCHX5+R2U(KwJ2W6K}* zwxg(bAbcY{A)(rf(rdTD(PobnBrfPs&B-6e-bDlrnwf>ztO1~%3-b>J(3)w5+ACH{ zA?FufxZv~EObsO$YE_r7GLi}tjVwb^r~LWnYaR@x7-GE>9nE3hW!QqJxgG=Wf}Rm= zxI2g^7;^v1pf%_s#J>ha1Zi*%^Dq7__}ANi%D)WVgX}eU2IzGNaB*&fr0ZQp`T(gO z;8v+beK^2NL7zNCk@y4-mQj+iFoV3l)E;g#+kTDr4iytZb|RZ=B-D0GX9>d^PJ(OY_VAu1Z^;nCS8)Fe}j)hwcp4h}#_n8MWt20k!p3kIppBa-$>pl|$QHTRAJ-mmp zsKSUx!jg?~9OSAD@vmC^h4UGiN8X5g!6S$Nd-f6I9b<#<7~(oFUa)veExWAo4%(OK z9W<}^V@Lfjccx(fh(88dEJzokBSEg|hy6xA;#LG5IHG@K9>#t~%T~#TSL=j5 zB^ERy3#=(6$ z09&(iv+f6dupLB<2sB2VNUFzmy5@yx!)^&`f0tS#|S$j2v!x)MTbi>bjJJ5UU4Kw=c<0obwNENZb#l5FFMe4)_J*&DlWrfj1}M%fb@E^2g%0@aDvu<9)s%V(Hh4yiS}^ zeiHAC-B>PC%XWjs?m4bK5^q-md{>EJeD z+<}3Icc4u?A9Lj{y!$ShNxAFByXm?sg0GWY(o)}J+)r}0IIZosXoFouV7umAQ~S3v zeR2D7?`X*6$fpU1Oit!Oqea|EZzZj!0=i1ei==Y&$9tZZ7YZ5%_cr7GW_v+zLje$X znDo$aLl32QR)*e*@2~goqwn4L9`ppsMQeby5JmV5W+n}X8d$r9Wy~k83FIW$S*PP#JjnZ}49Qv)3I= z(&4+}FgdHeR;DxIq{M|h_tbnFrYjoD&Lx5YqWCq-i5ddL5Cu^kw~l}Q09u~cxL#EE zxNCb%pD}c97B->B2Ht%1qna9O{8l||)Ckp8Hmh6bnkuam_>2*I5si6h&ong+{_rBm znH3A0tZJf6+uN>5VUqz(R7_wKu(#?ar<$B^0)I~`Y|P#RxMcA$!)Cq@&w(wP^tXxO z%-@XXlbp}bM;+aIAvxcQsWADKVM8Z5pX}Kr=Wj6OeAuqN9y++leqKhOQt_#x8s>Gf zK;gpP(Wxb=3scDg#epTkzKssP`(`m2a=y8DXTeUAg?*HX%hnM5j@nV^jtI8nX4mhK z6I-|{PNkerQQ*gNrNpG9rnF1ZrdNz_$fC4`PFD(LPj>6`LX*P;(UuzY~24tkMp=0}8VH=0RT${Oq)}5OyS- zIh51Ie|kvwM>=^}|C-4o7k9r1*G%5U>&yD9%L#9pu$M3QzIINhYv*)%=D-_3qUwtr zn0ENqI%;o+I2@w%mX?WzB59NCweZltr@7x!#KAO4O!Na@GSktUWTk;dfP*^99 z;jp#@Z?Y?%;#iV%yjzo@om3~E0xAG)5wb=a85E^>GX7kAAf9ZbND0h|5A7Km@ILVyyo2|KyLoT8Q)Uug zgz+qvN!-?+fi}^e!EJKd37ex$&4co)YdqTXDeoE79_$%^Gxv22 z71}f4VDJ{=VB-&~***;PM&2*)57~$LJ=}K$;u?Ktj0x^Lcr@SN$?SV8>U*nmtpcs6 z@2yf=<+L*UPHTTP+dlXmuzlPgvJJFGUs{Ly!p~>h2R$FQk4}c|qxGrQ=UbC~xOGfx z>PBno1`gB>niKfr35yitg?B*ipCeYXKYoJjNthV8j_2|9g1|+SAFU1}d#}-^!LBVK zdv6=GsZH%Rk^kLj(_r}D+1}gcK%43|r`nuvL*rX3h{PP6*z#gkgc_!)DJkP$eHn8=HYHA7UE?{}wdxXh@H$WiKNfj2d??^dUTh-}9PY zNHT2W9B(wD{Jcd*s2{#Go(nmEs|q3aoK$g3%;l4y$^l+!Q^YB?)ktJTXs<2i*DxWF z?m)%^m<=Dw z@Fs4tF6Luo?r=hPct?{)6GMjsrdne+aeyz?wQc`MlR%;n!iyVlOktyHl)@v41-6gG zD*}DwQFjRJ9XNGO z(d^$xZh@H<@7T%VeD1H?v~v}gDm z$!>sWll*YUZ@gBF_R*R!+BXE%5Q6?ID+bT;46++g`$&FR2W<`U5@lrQs-i;*e3N?% z>XTgyl0twkk_z;3~@UYu7lsxrvc>i ziJ?yi=-NU$jK;w^27Q8KaI=_%b#2N}oQ7e^P`1f1`lVf`{~GMq&8}ZDt5GiRNg|Y! zXt2@$HL!sLbJ%!3+rXdUd0&p}?X^zE@9DZy#Mi+A-BrwIU|hi*4-z{$I4yNdB#a}2HLhB%wi0sQWEv}Y>s_19AS;SXIJ+UuDK@_xrUC$)bR zbgEa_&yKo@e1DGVXOE)&F6vxVAd3C$Q7KV5Q3X-`ql%)6qe`Q!7y;vbV`*$;fk&fk=K=WJ+XCWI<&A$fC&N$kNDg*nFt| z$L>gwK^YF_*cV!Zoj^WBgkdWP|=Mi)er#VZ;VU4#RkEZBx~ zAMhZfv6EPl_(9>)sUuR{Jcp}VppLOy_HIi9oMS@k2E%XF-Xti;hjsVVF_+Oq3~=Bq zY;Onf>~u&q7Ulr!pVn5uz69%pjftf8p?qk^&626;%v20%sUK)xl|0!IZ8n7o|Y zA6bkYEl$i;Za=tdLu!Xc_3Czy>(V0xNZDxkkb~;emjgi+Rugt`O7ur@O@R+L;J$}- z7DWDkRwA^j_?vwfqz#cf9|0y9R0n`=z~8x62A+bg}*3MdMO z4Iw$qZ1JB0+}*LWaL@qnvPJ2cSHijLSrE5$V%ck{-8K!o0^S{Ct-FW2^PyM5=7bn) z6Uv!|2g}OAZefxnkgy!gMI=wIUCjfEMPSd!2zjm-maQZ1Ah zOjKioVI-?Y5&_2}RD_oWy=(Ng;r``0ZZ zEQW%*_*EGJGVE>$26hgIivA6YC|aig(K_C;hWIL)4psD3al_Jv@Wc%|Bd^PjNrzl!T4M{Ts~v;d8Op+TS{2GCPL&JICkgpWvmwW*z_Cd5mV$^T%RE0?D`F^ zHF$1QXMdSCf~YtHwUQX7$$JXKDThk37=w2MnZZ1>UFd`+EM* z?+b=j8s>SpxbV7gve=W9s)dswgiAW)=O*gu*+|9qt=TC9Gv$-|@JHf)`vt-by>F4W zUHOFdC-^PUKT0d}ZlOad9ir(_h=ZrD72`>uBlrS@?g;`qRP2tZi~t!&6Q?m!XhTJc zK2Wr64?Ku;rL{e7?A25ExM!?hJ;U8?(BQ6k|5oj5ffxr^fPgU}ke4qIk?$yl%xUCv zPMpxbrrL_jv|E&B`f1qWlOgfX10%2REu=6$-dz}+qCo8ra0(Bq#RfCU$qSHZb>Pn- zCYH?}K6KX1VZ&!nxUN;pE?ru-x=wFAWcswB!=_9b*6P~MZQ5LS9gSHJ_zI7~N)>O> zBg<&my6qJsVOa9Shh7P5ngKk+bp(OFhzw(~v}gY|xsBR3Ov@PFrD*o-(hfx(Tegdi zoEzWZuCW7FkwW9TPy2^@t^Nz%h02}4Ehtt&NChfpppp&?aqzgU*HAa%Xn=*wo&e;` zPUUvdH1YbNU~B{Xj54^?YiHha!-m{eou=G->(x(pXwgMKHfr&0UE4Oi?SWBUb3iY^ zrv`oAsQ-u@`#OZaXdx+}S#Y@0p^RD-j1FcP{vcoixkI>#cs(H}WUIN!YPvG!OI04< zr3^i$oZqDX2owc)By>qm5XH4uE)(Aep5^&QOggFr1}EA!y+J~(HPOjddJ$~mb}W&- zw{|PI<>ta}x8B^mal@>vhK-wl+jZ#Bt_6bz6*SDsY}7OxZYWXnzV?dL+)1EKlJRYC zM)T9vT&XSbyrR7VQQal{#khC7JK4C`9v@9zRnz@H?tN{xJ6S#fUQ@GD`$wQV<`mj= zjhmJset)ZWLauS&3m%sXs&?hn_wcqz1RY{3p5UT~@xW8hns&S*+dzxSH|yH_i$&kA z!+VFV**vVP&`xa7QZuu&rc4;tvv&l-?=Y@4t+g)#appWi@`crcU1uRVVsCVvlsY-6 z?pTEfx=sv%HG1gR^#AbdkGdQeY;=0rkz1*&3X! zJuXKCI7CyKbmD~E{Wi5nxZc!$M;)x`_+3L9Yp%_}g&aapFh)|is&|iaPnS#ScdF}g zU{;NHzkyE_o1Tra(`oFg?L35BaB%$v2M-Uz>j}3i9#f-^f9ox&96I#iiPDD{ze>c( zOP0QenA+*sdjt4KZ_n4e828Kl z+#aYK+81%cyibb6=GZv`(lm*#d^3o)2=I`3 zT&P?GSe(MBYf;JjHee3Ucdc|WT5Y=Ex-S4SmA4;Ol6ERdhqX1AQC0`oF~<9;E+5|C zF4!LOMZ2jbn2ouh!wi#dA}Jog%#`PFpg$0gjTVhGc@||#G@uiKQth@>ps3o)owmCw zpRpDafxVEJHdlS7X82&`qCaeqy0K>5h7D@haWuvQy{9Sxn|KPeY#)oRt;eEOMK1671z3{$zBn!|} z9q207E4*PE*C>OD7_oEcy6t@sitJ)CND-I;pg=+Xy*wev+#M#A-(`0dSj$qJlBsI7 zKT8#_EARL})XUc>*DKerDcAkSXgt`a&+UUvALn2`RJRH3g&hm?Hw5%VII^+e>FHE~ z7+SY!qUWeYYovHSF*x3WXe{%h=;CM+CAs+!;vh4i)%FDhOTMkv9p$V8{FKxuk%S~- zS6E_t9$ZZwT!1=-GcG?rHap9$!yOkEo1D~9?eG=KgB<y~(Q`)#aBbK9^^-4?k2zUYUf;asRmI;AAJ9egxf}Y7 zT3YCHXPzT?Ec8a`a|8IOt@Jsz>{y)*VS!-+xfp@k}Pe)l2qr4oPaS*5ez_nZ8scim#plZ83R#Klu&DeP>i!iggT5Di0|xqW3JQqvBsPPCQT zkqQR0FElYBE*WH&n?*6@jDnY#poF22m2z_BroZld=D_1+GWOe&g>WTb6!{D9IxnNQ zG!QA5?>&3*;@NvI%Tx8YY`9(IZl56U_7C0vuz9^F-|xd7 zeYg7#bC1Se-~LyotCe_u7Vq7Nn1b(c%#eN1uS0t$X}=9WgA<6221f3{Lio=cV^14r z+{eUaX~_5M1q<7hybwFr1v0qKQ-j~rs~_ybWaDEyfalG-^>xnGm`|=vn|r2 z8%8EK>5|!JT>bp@V-KA=d3bzzey;xoR3n&W?X3U>@=jzrgZ(j81lHxMm_u2Eeu5Z)&;G*|`Xwz19);&AU`Pa^#T(qKW za?KkZI&EE8@+|fjtjD&ph;i*B5u5Ft>EM=YO(z>Jp>cX6RE9PJpH!+VRwx@4L-W;f zqHokqaS4qFG%rx&)kcTq13vkeBfIJ{vmqJW2wnm%|VFzc>*wTv~ zqw26+OLxaE*y9pA6<+M}NCc_BeTpI+nwF;EXJVZ(`COBmYcgRwA%1(;7dwhvu zJU#Yum?A$GGgm&$F%r6Gty_NA=l91<+iB9rqE%Dj{{CL6p5B>c#zpX$@_N+zf>O#{ zo)(6GnB73uNLofzyYB8SY5`F$K!@TMknyVMP|yNKJe*FaFA%6oha4O#DqB>wfQ*XI zu$7+39*MPS)ij1*IfYGC!l_qj?6P1RuXFqUn$-}Rp-q`Q^ zj$EEFcKGPChhMs=E3YgcH)HnHyWba|4;|inTuEQ|wq4Jyy>($~`kda6ZQs4z-E&M) z_Zx>kMsq7%cgqp#5$sKAmVD=!SvK8TVQ1{DBYtmpjbq6;LWD#jF!iYqRDB63YNiqj zT^m!HFS66})FX1|qG^-vNZnGQlusSlZPcJ!I=t}m@>Qa);(u88l*j?=&pbTip3;TI zJ90`>hV|^w>7%D-iLTFxb~-Hyw3+!P_*$^$1dBP=41O z*{Veu!Et+D_t`??-aK`Auhz3{e)ldl@51&-^2|)|?FMRdra#4M-PyS4!z<{Ml15;_n=0OF|APH5t>D28Zo zB!Gq))TAX{`1s>X7eD&=qCZPXd|>h7`;|ohcMmRJe4o+^6^K6;Ek%;J8Go(hhw|Io zm%mF$w7@9EBEcpv!5fkSl?17gh0(23x}@|?8JRLAWnRjPl(5^pyKtGLZUJa+7{Ai< zjKWSvig$h(7bMA6!@@-Tr{cc+)Zty~bkq_G>MvT~Tue;5p>@58`f6lmD|_|Ltt-1c?#0sW-*r^aK0W>}zu&TM%B*WUe+c)^l*rrdbb$>LxkWX>8T@BCG5W^(0b1YZtg7%_<$4KcnW_2vXkTE7hXa&HlTB zWU1U&Ge+IKk@i2TGnD~;69cP7FQbKQ-L+bH#YA!<$OIcfV#(TM3pC8AbBWeKE1^|u zl(|f_%3NlA>breizE6k)XMXZ6T|Rda%1SL?A%6W%p6=4gA5c$;3nEFrDgXKU4_~j4 z$GZ1<7m?}0HAW5xz8J+!nk{v(Lk`Zd*de2&nPg;V>5Ux#$Y`mv$R&{JLySF}o=S{*QAPexl!q&-p4J^ zh-*Lsri?KUG(cl~qcO$~zoDfMLI-r%QJ}ZmM;YHbM>)+dQ@HZeoROH?Jo(+I@raC3Wix#^ z+E5?dcL-M>4vKu%yB%{A%y~1^a)+MkuxojeVmwW0QMMKFUctp&oW zFHXFA1FFzcagV+X(zEvwO4oD7-g)cu9fgs%*1xm<-1^Jwm)F-h$T%gm2f@XJnCWEL zmqk2-;{|sXGKs0i)&p_r$fC6^N4n1*lDpnby{=WgI!S7s%%~a5JCwTRh4)426KTU} z{}G7=TJNn>{w_ZCUn>)O_fgbs?uaETMQi^FwVqf!XuQ5wuYF$=TcF+IYk)b`cPJfa9(*5VQ?G~-&@!i{gI8Z9ozW{5A7Sf!VGv|n2 zfpVcu!g_)o;uGle4-h63+1%k5pB!il4*_8^#UjS;WbCi~?_<3vxa7`Ze-(!UEUQ9L!-iyg*aUhS&R()3i*ZS zgl!X)2!;EV$;cg4$PTJ`AO{onWIv`fv!IH@UL2Ga?Xnr9@g|{VLJ>?Ep;IsmBU~xb zD9aHWn{P^%Ss9s@2?^#dCF1q9oA%18fBq;x@$D_&v0piX3OHwa_w3g1vBhq!?sd;r z9oZw@@>{t_j1&>5v*w@tcFhmf;g{u6$e>utYxDtyivA4$36~MDY$~S}*L*NW! z9uOu-H8(zj9Y8Egrs8hOSGjWsHf!H7Sx<^e%a4)A56a`Q`Kk31^`!dk(+AA$;Ejj& z{_(ttb?U8C;oSGn_U~7^DfeZk>*~6CbtcY}lZ~3{it8q(z2m-40A zGwzusddu;92X6HL^X9#`joLec7UZQ%ySM$w>yCX^e~N9)IzGq7U}}um81iw=3E?+@ z0;VID(TusYa^^#WVg|klSBt;}y#g|OW6R3Mx`{<59Pz|r7RK3H0HmKZv`L4e+N&ow zBCIQziKJ6wT~pt$qBtUP7xSol#iOA( zBKBeMSi$E;TnI4;f$w1_J_HG2PB4y}ZuokMtA5k><$uAd>iR6*DJQ~IH*|f9rtMl^IHzy=y%-7yq_vrJYc-(z$^FP|Q^9yIInRTK>NO+D49ibM;O3aUfzE{2$)0@ZMMhR1bbqu*Ql* zZ4L5EVmLvW5jw10X@vl9A1>rW*sX>bOj~{$mO==(HceF&7+#@C#mCZRw#sT3#Ds5z zD*fO1kIQf&KEB+c`D06kx>!tDga34o|L}!Fhkn-+{XfWJS++qm5q*^B{W< z*`z&|xt5MRJFM^AV86s{<)BYYJS&8h-}sNgOXyP{5EH3SYG>sh|GF<;d;L4*aetBS zsqy>&`N*`AHK1eorSuc%OS0uFWO2`Gxm~qRti_t*s0JLdCZxmyRl|u@5#&ftXbR#+ z&->5!-8||1Mc?xEw>&k-Cnqr*>YtEs{Cn9!yt{p2Qi8veod@Zu9Lor+2;`)D#RDN8 z#AC@k2oAv@zee6MFj>SwzQ4QZ34ZNu}<`;w-feU{_?4&^ZPAyr>Rr?og(fSpd64%8>A59CF&pz7Xw9_($D|G zkMgl;+T2hdg7G1k|Hcq*+oR2it~(3~>52wj65yh$y>(SKLDtRjRv`}3u8$+?h{Dg@ z)!uNeF0CPy_KjxWZ7hP15Dmy*n}#YFrl1MJD?U&*fG>)(`({&DmNw)edMK4mul4X*K{?Szwb68lw4*L2E_XNjdbLUo&4qis=( zw~D~QQh=!Ft)#;_IxVFGp@xwo*T!wVYJEmMuC<{xcen&WQfAR6EiJTpkvW=Jv*js6 zeW#4vR82d4|E&AU0D>q_%h#}l9opnQvajN$m!5z6#XT$k`Wif(SDg|cb6*?9xE4fzhgTYF4 zTWICRAgHY{a-=>*pQn?bOuwx#qLJQ8@2vOHQ9l@z7>SC)36U9*9U^-~YPS_e4vrif zd3WTJ$Tg8%I~vfXvDvUU=Ef##V90KNcN{3LMH{AEej17YyP#p1vKIIj^3x2yg^Zc2 zLVU=QGfB)c))Hf{*y3@t3+QLG!@g&A&0!%An#{1p+UtSW4s{`YBTa<|!Pq_HwF^EJ z4teifUW*6k2#o2a0lYt!$cN1R(QMbB#OWY@3hj2p#hCp-Tnu;u7M#f=f^jT3XZ%Ct zx4IGGM&CCBPocb0H?T)&ewlakdISfgiDQk$>bKQZuLw`XDyvuq=@m{-gn)p1pue_) zZWJXMUtRHF{06SB;wS@cm}O7~TIt){EqZ+^)y~V;HRv9p*XbU6!?jt>W&O|9u-DY_ zmkX9Ga;uLjO0NM!v;wqGMf(owKj`0sUo?fB*q<>$5^}tS2)y^kB~V#x%IAleU$+hy zs76J{Fk)}7tk5a`gx8B7EA1L_+T*hdJG^=(0&qL%-v7_ z#OJ&8`5|n7m-m1jhv_kwzC(l)ponOmVIiUTCiwQcaNX!XEM*5D4$?csMw_t*F9c23 zE+ntL>nrlk`RSjB9$Y{1z*wSd<<=$*kNqItl^u4JHg2T6Z152`^)-?rnXmkE+NSJo z7dRWxQ;?O;%sh;T0?#4+p)lD$Vd^%W{ zrGMM|2fsdiKlE165Ev8JBn-aN*cPqVmM84Kq}ks5opcL`g|sgxfoHykk0e*X&P%=J z0ku|@CCGS{I)tA@f(PVrJ3>k=@d8duhj@W1GFK2Ul9a(>!nZ<^DENL{PN%a+)2e%Ybg*+xU)mTkk62W)Uk&SsU=QX$lr=amB!VQ*Qm{AY^#H6gWSp$ z&Wxc}yTGshb-N~ph7fI{@l!|g=z09H*67iCdJ%ehfc13DFmjVP=RRi^959Te*3}&`I)Y~?%lE7 z=YK=F&bNJsSN*&D9g&O@Lcf41{Vbml`U&@jJtE@J!14x%M5Gti3B^TNXs031B*H59 zWY8PT`ie!W>?E{Kg5DSEY*5xI0w95RHx3n)$qLJA6*62aD8Ch%=Ubw7HZu#MB_lxr`<^g&fOqn^CJ^rYUmWSWW?^~5oiFoa~>G>A7Xgjr_JcyqlXS1 zMQI#o1n)2_ez%$bek_eKH;6ibR@fqctChxx@fi!Ac^8O}Vh~Jwo@0eDKLOYBCb@VW zB;`~@C#57I)2tw=KmM_kn^)m_-^5-J9$#BufREg$TTSX{nn*J?pfu6peKB<98WI1K z_`rwyL_Os%8=u&|UPLQ*b$t7w>aW0QuTegGY{`=Cyti{M8~cBI@Om+9(6NxYTT(=5 ztU;yLQuao}QtHj2gDaMjr6Ag&C$aP?2^w{1*u4!A14-XIVQjI^oiMiTgGC82FZosq zBZrh7QLYVh6AgTo;ZH|iKJBrh&QONRhX-E*k0>_o29x| zvyVT>cr7=KHDMK%^{WVg69AA16`=#84-^BCfPgqn6m|wFz%~9BF(g2XWXMPg&$9_W z-tg%9=IF%OWDan2M*t7m9IWcY2y+W6g_)*^k-kdo`sn+SL&i`0{E+-mw3=E{GF5)~ z_59L>^S{zMJ^E|5U3~e^}rvm8btH7R#+aiaK+Bb4BEja*J5}Pw}B_MSse*!Y{jt z{rso;-{kg>KpgAy95)9PS0Y-rpk=kIB#c+y2_O@(TkBF}&vI$#)1y{DL zq5{HOP&U{Rh?!snVj}ew5!`=>jvTh~3iYn!`o z$=Dl*57^56C3>yFd_|&aD)*PyrPW{F#jTSd@zStZGf7NTtM+a}s^s3j+DL7R29U?O z+Hw@MH`Gb*QotG%MszndNo+c+aRC%>Qzg}WXpyqn!GmsOs2)<0rX}IL=UgFfA7P$~ zFR;AGp0(b@E{Op%sG>tk3}KhV^p7cuA?y;mv>S)Lqhm^97Sbgb&Pdgy^+_>UU3oqI zzk=H-D3BH7VQ!<8AV^tfCl~pA)5Z9&RXIW&QGd4Mb_V?KBk4>ayTF(`r=Dz0S5R-D z>9WKz+Ba(J$)U{!tspiOOL}sCJ}5C(t?|j#JC9ziwU{)R=Vr5fPacrRpB6`d7w;M4 zsSm|?Mk8y5>4&MrYJ+3(1idBB2BCGq@F!S{M0G_ESPK-;%vf=^SR&R4SSGN*>QwKY zUuR{VO-Rl2P63!kMV~si)&Z5VE<&0YTP6&!LgfM?$A1S+RopM1cl)GJ)T=KaQqC#< zXxNhcE;QKW)t8y}w+8K>OxITP9lXohu(+32U%Xog4KzT9VuCa!oKuKe1;FBkj~mI! z@NrY!q3IRH;iYs5w<7Z#*Xftj3^-JT(uP+Nv}h4MCkF@8=WWvzOqXGOOTFxqO;M}v zw2!npM2WN($C($`jybc|b{KMYxz&!&U9mq{vv!ki@Wp9F2u*zI)ZpF(9hzdsS}^Y) z%xeqpr+QlOCSGm4kL}skU|J+--}pSxX>Fo6?42MwBP^I>uE76Hj~7cU$_-;4-Gf+Q z|Fhp;y8PY#pAe3<;?DAAOP9?a;a>4K(cx^%uSMReC){I~pWN2+${cUl2bq6qCOR}+>g$Z)a!Yrou84JL0KpQawVP=$F8gCg^-GVXi z?r(lWs;}9Aoc-?}eKluLGhb@27O%al^(bB3w0&YiZtCLsHH+?E+^AhbLfb}*?@=Pa zbFZscS9~FpqGRP7@?zrm@=Q!UIb3Y78zq{2pZMC35#cpIg@qm2@zmR4VQQQ^{QLbF zA9y#^<2*jMGHsN}&Dugy=`992DUQ%(B!thvkK{ zkb`!Qv{>-Q`9A#r(o0s?)vcEg;cXN%{=EBse2NCuJM1i0--C7*`xqNW7L2iF+987$ zM8*gt_l9gdJnF%hd)~ZsB=q{4;j)Psgcn0zZG!#vulQ)QxA4qbO|ZCt*A*Vgk~)V1 z*uA6Jq3rG)a6*T@1r`jFiO5};mlm{PC@CW9z=t32mu=;n3ZkVakH2G*a_G&Cn~tbk zP$F91HErmiIkZlngFR>++E<6Q6T;9n)+vv@)kq#E>%`L0n1fnWO8cly@9;qXvMC%G zxL_-hJZ^VtPPf|W)3SWN%O}jHZr-=%kxJTNA@>}SkKNJtMsh1*JV{=m?TYLyu%2D1 zwV?(&4OdsN{g@+Xv;80}z!*2wKIK6(TM}x8;2I=nz}M4Ui(H;2#&|g-!=MislVwwD zYH@dO4nen7HmM@GHhhNoMuz|$wwRx=2bvIW*dPJn=0Plh&0LIuh3*R13M6Aw!3_$< z`N+FSLol}zKC;ZKxC2jq1~&BRO9&Bp;l6PT9t>AysVG(IJfOZXXSjR$r}C{cEx(X& zouu93;0k?%zT_nyZ`ffd4muva-4Psb-tH}eZ~(G~2m*49l{#2a>8f&3CtcJvIt1uY z>>{0(4h3|mGOzKs=s0qT=@%SRX{Rl?J8gU99|J_vIwB0jW!50fW--zk0gTuWS3=7-ul4yJN$v%{G(C!B&t4v>oN3TwNWTm9K5e+#&di?|8J zpTF_9F=IpnS##%Iv`A1lWTg0O=GIB8UzRWQxrvMOjKB0?46Xjg#5h6@bgXlcqs`90Uu5YKK%>(DrwP`!KH zJ$=x!R=%g7_K5^-pZtkRYy91R@~IcYBi0hWw<`X!uwS>De`x#4eLfy@ws#P=o;Zo| zq}l<5RzvmoA_xyD;#@=^LZ$kl5h)Ql5d{(bBZ?x5BT6HT8g{~UC;YfB<}Dit=alK^ zKNTCjIRuWng$@Ba6r*HiMOEXIjR}IPF;*I`Lg=PXxCugc;{tqDQQ5evF$vxHNkwtv z(#8OfBB{kK-yr2(L9>=jOBCzL@D>z?%Wt5bL8rFQGvKX~T0)va?j-2Q~mH=@tYC5Ofi9ylILENgG&4ft>WOUem<)|BFsJ462x z{|Fq#x}$hB;vYa@L0p3wlI9rqASZ;Y%)V>Gtpw{kimJnTY?F1!*JO(?$K{D`%HMw5|3ffF4Q>9^rz)APkT?oEzSUv#3*E^qZACo^*Z1w>E&7d~nT8s69S^|i#dCC}vZ1gtUS``jG zBm>`MSmR*j%X(u|H0pXRJS(wPl!UP*mh=yDEyBr&)O~!asER!qt4zb*=8e!&=yq&t zHh(-}VAuqA%Cfa-Al`?G2FpSJArgO98`tVBJdD2M&K#Ql|Su~*(me;jvni;|1BMxm|HsL<9}imeXr1J;a& z{3vLi!>MmcBwpRk9&JP>!!ZVuwoerBFQTgpV_QdeiS8RcGI~n%yyz9t8>7Sj*xu~) zFy-vzgVSal7WUtw%SgDK5u{#*4r63Q4@We11Bo8F8=ztT-M`kdnHB?cSv<1HT!pJU3eUnBeO-Y)U z#8HN6hzrF?8{-W(C0S4~;$p2>AUe=+KgvVDS-y_i=>;_M%B||qO9QO&7cWV~>DTPU z3=qA%0-Mh_n1gs^Zar-D#i7GHTozWxc&aReS!mv@^=9qt-pDA077+D6BnVRfYxOo6 zy-nVNx-95za^K{U$y1W&C10VphW?(WV!i|t?u0^M{XBK7h3&c-PAe>5+!OgLjJYO_{iyrvGo%- zG-VrgTIeD%PizjDH42HPq@8R^zo+w#xtK^yZkpCN>D(nm%+F}xi(Rt*0pEr5nR2%F zvVTh*(b&IVZTtSWa;3O?+k%$$IF3YU!D0+Y}=5J7)aK6rDm8O&gj$GYxU-=s&(f z|I=zm()`(+2L}Mt?*63KtoH=mkWYDEeko4>*}U(YD6a!Sq>Il{(vau=M&M#>51WpR z#XPEw)jEhS?%3UeorJN~hC${nrNX=C=ukz696F@523m3Jm_4mRGwO|Q)tVRITCTfSYOpFmK$jy6gd2 zE!rIuk&EwM^q{Z|l~-xLmyOc*{kPZr@xc>X<0a>NTa)?^ss;;foWtw{GDn zefv!3yq-gwmga0PUbOJhnaloCuI?~koE2AZjMr>;Lmsb#e;6-|LjG*LiZDPGm948< zBStKzbwO(wi?(36ya75yQ*ELG94e|>pQNvf&95L&S)*G~+`1HXjQ;P8uPyH5PsTSs zIKCT}Etoq)zAthP{5Ij%NyWb$xT^28Y1xBi2hG7%CJ*k@b?C;YAL>0b zJ#Eggwa;O2hcz(<_tqN+4J=n1TNv5U+lPb40@Dxi?2FkhW#-xAd>eRh2tU-o1p*#E z@gNqlELLR+G~RkD=^pbCO;g{y%EJD!$y_jV+R`w{aO{K%jR}@bK!_n>|0pDr>sAV+ zt~fzLYdWZ5pX`11s2Wyt@#wQJeXIl{4Y8w|F^1O`ye(V-*V`vr9SOLQBd8h}V356y z{){sE}sie_D{NvCz&?TD;4R0nPqF7e@JghAE zFJMTFN1AIgxiM}>wKT(GPq-alE-VXI{Ku8>vW4>>id2-AO5`df8`aS)!+qXQa>mQ6 zb$#{AYTFl<>-vKTDki-O@z1Cua}d&JU!y;cOg`QSt;q^9Oi^7JMKKuT#0*qcc|^Do zWZ_8v1}8wac6`TNUnmFv^oY5uF0OldFbw!gNKob`-UJ`xLN&sH-xE)&lb z47hv`wl!`Oe8XBjtzRf2o10;+SKh>|lVHLg+7!`27_CAK(cOScuUIHnQ3eFsW;11l z+J>ZZE8Coe_A@|!d$h&ULCtdOCn3K)wR4pGrF-#O`SBqO9O)-sgtB@MaCybzBVK!+w?for6;0^>&b19$6+I}Z|h zjtkYfX|=JmYE7V{NJdb`q1&u*6@v7zEi6dqdu-?(G?LA2ixNsB^If_WKSl-LhrrLErvGPx_|x zzSB3Y&mDL4ndZB*_Y_)`o(G#OTF~HOkBFp&xo35QduKO&&_e;h3x_oxKe@rEg>TEt zM;4B1FlA!nVGG|O`XQ{o2ep>kUi&z(vSbkr6S8&UPzHl}>afe$#{v1w_Hh6=vX28C zaE@^Rf_7GR7BGQ=LjdO+hDO->o@W~qjZF=9> zx~H#K+g|rhYBaO_r=;`cvm2F6YkV(0{=DJdMoN=sOL{h%a7Ux-OP)>IIju*d+lm_X zoVF8uifn=7!bTv*q7bMd+YCxz51d5_!KIAayOH%cu>qkJiPee{8eu^)KR~vQ2V7SM?RqR)=eE~!MRtC6ZlZSF{rqR2zcAg?KG#!H z@!6-l-6M<17I|pt)LW)ayXB6yOXPV*Hmr(X}n ze_wy96rT7>Q&z8Agpx)<>#e~OYZ#k+dE4Zmx}cHOT@dOksjD<>H^eOqIJzS?l2vku(@lBLDde! zc5pLDMnF(-M1rM9JhsXK95;m95MkS#Q_yUPS>!B@j$^YIY!r1#uFXq>A1^&mchYE( zCW9ZuhiaaXFM8EmuVfB)ubVn`!Gdr8g-u*BY~uPJ?x$&6aTvn?(|&Rji_}Phq5eo#2Ts8UN$MTW``{Lk^yX7KLf~ch5v9H)cFXUyI zT_G2Zk&75Z?hoy@EMGnujhdb+ zTE0V=aIkTXSpm$rEfmw56e{3<1_Ph{>xQyu8b6EH^|Rl zeEuKuO{BuH)Kv z^$EQO^cRCwW~)_!7bRxrr)4K6iY2>+k}7uV%@&C7rij1s?=I`f>M!m@$dcLYT?m%O z@vxM@H9KJMi?x;}9@ie2AC}3a8C1nQkv&ah^qw~VFP*Of4(H;w`ucfyP05Js&~YAg z43`itJyHDT9*+D8w2e%jwuj~Ah9!py*Q=uQK>f41D`s@V{jK#xCC$3OR$qA25c7B& zNbEKP3_aY{Ssn~KuJ4BBI0aHv0kT+&VBai77YPjvNMiTHCJ70FSQT)5UhBN6enSw$mXodq&z$$HnZKVmqA?vu9rD6obg& zPC)~EV(oL}r%|FnFi^Jk_y6g6?dPdEie7U2=xLfV@v8RibHrDQK5g{q>6$XBU2glf z1Nn@;#95B~z;Wsy?)a&HD83CnNg7YGetWIsx!N!8{u3tjcem=)sg?ZBebYpIZhZ|t zM}$86FNQwLFWdv}xMKkBxu!L$mxO-%C-;LE`pL0jE8%m}ToH%{BMgFA7912XyHi*K~m{1ZMC4A!5Y?M+F`Fdg|R%D(o@-vg&$a~kbGu3Q0KVBrq zhpF0t$T_2XcB+em(lko+lTTir5hY&`x2PKx(X~gW4AlIh$OG4ltX8p)bUmbwj(xP7 zuS-4ooVZ=vR!UjKy>cmn` z#ebS>@dqC&#uTGT>d61|KGf+J77n%6p)p$)0CJJp5~Sfq75Cf9+F6BJs!MQw6@v8*kKXvplMd>7#uGg}WYn!6u$LTQaO|*9zG4MhmiD zU`p%`Y;S~%ufk|y6P&yWjiPa{H!+$}yug@X(q~H`*mAZzJu|I+es+E`17M_y**>Ms zS)ok5Z^oL*kH~dm;!T~V++Ey!%^JB*>E++2d(Izx>F(7Nx3=4mbnnF3-EWyqHYEdx zwl#F$MC>uM3>nZ7jmxr;wMVnoX9d(*3a?j*qYO!JoiK7tK_KFMN%($k;sj~BsUbbc zo6&&$#oo+RLjOR=5{w=pdJbL8MapCdyw(V-H#(}dm@s+9q~SBht9K? z53d|LD-$@j13OKbQPfiWdTQl8lV-H<-ravrKe6n_{y+k`wC{(0-Tl9H@6&A7q{+9; zL$|RfKLNWzhFfA!&IzK&SQ`+p8OxOBfmUuKJT#UJ@d!K8GW;fWcGuC2@p{io2JZ{HNJiO7??*>p20@)Ot~_pTp~a=@C*7xIhIj`Rc3#; z)hFxSUe1T6{ANmR{%=|_L!R<&+^BeDqDU2=!AbL3xp;Gxp% zY;Cp{85?BC0|jHd?d@JAp0TFDEWiXJL((LLUBo#oE|^ysJYy6K1dqIfTxzNUIHFyC zIpbE=E>_+?T)~y6WNUd8c(NDeH=6R$yOl?d`NyGklo#-Y`Ub`;k?`-$+zSU7Vo>1# z{-L9tafw(D*bMi?)@~I{m%t(-liaH}YJwfMAXqP(%c!=e&p*UDl}Wd-zl;Ijwi z*XfTo+cbU64%rk9!9LYkt`D0H+j5q80|XVOu6GY%+gGAo?{1E~kt}iAxUQ>weU=== zct2U^#P#)Zy*8bnk61Li zu2Kx7vHk)Wtptj3H}UP6@mO=oW=ZQLxC_w!V67kn-kyQ(D+%rXToC*opO9-$2gbdJ z$tA+Zd_)ABT;L!_mRlTjvZsy$WQrE|f);!~~pvaJf* zJG9XB;gp<*8d(y&@h+N5@G_b@GoUMxq8E&$P9=Y*@7OWl*|Y1Eh!d*+9c7e?N_D8A zxVU`HnsT{QS*rW5_b2M;XBFnK9pXL{D4rZ~hUlj`lia&2PM*bXjIs;6sXLX_6Pg^d z^VznITAeVG4(&aOV>{rLdIH2Qadx5a!w-F5f4x97|6V>Mp87`oBBQkZ<&~A?{tv`X znc(+gO?5{M!g9=24C)1jsLNVI9AsdfIA+NT!SIGFT;wGjm}5watT-1Qpe#vBkmT%1 zM~9HOXiMGlMhOb=G22g>Le?Cp5AF2H4CV1!Qvd28YmX7QgV8 zQuYcmTHtf0v=*Eqz@D;T5M4{jF2!^xqeB55&e2U<=+K|;JO@V{W_CCIzK9MvIM8lr zUuX!^vawAT7)Vqg;fI#kY~@Y$I{$q>|Fr;$Y^tm-zx185#6KVXqCP?n(h>KGK0fX2 zqcw{To_OUxqP>U?rb96`y2^DD-K!uQ0_FOmTNPwUpd?@FSTyw$KS90$>t#^;{7{7hxDf+lm8eEHJeFQ(m*JFURKdHEK;po%f$q(FNV?_O@&U`Pfkw<_2=O+1UQN1Xo;eem6J+Ae~_;qrOpS9E+{yA&Nc$lrhw8jsc*8ebm`8-742KnYN z=KpaRDV!-ML^HwhYv!Vo9yERy6b_YvF;Uo!(3V^k1Bd8ez9AWaDca+=Jl?bO(q1Rz z4{xq>_ZqVG{-jokw~cvj^~0WSU9v`8^?SE`_chn`>FI9MzHZ|tX=!4hyymEgME&pG z4|IO*XWiZZlhO&V|0v&>RQzV2$U0*6l(}1Vy=>N4%-cjT}1p)>0H%?jXJn`0A+2QYX)Ms z9&v34ve`k`an~2FUtLkR?bYz`43|2MZe}ge5y{GV&T!MCR*w-fh$5D`$}}jJ4RvfA5$5KJP6M2?G`5xF$-5g=iDfKP*)=6*<(n8w#m|?>N_Wz)7 z^qx8AFCJRHZqk7?_(0a<6=N^Ann~cJRNU?JQmnyjqh-mS6Z+tXogv3MhjjsX#F*O@;XEqQ+Y9|V;IqZkv0I-rM z5A6FGl6vmKX^MBl(>~u2vE*w-8Q=8pFE5k7HG3@e;X79?H!MQXcWM1!1%FpL^ZvJJ z-r?sO9XNsO4auj)@(<+;SiUU6yzj#p+R3gLf+)YdE-c>f@RIN*W{C?y<54Ms54}}% zND-v#;!sg3ss!n}0!m*o9V&6CC=*)*^uYqUaE|WXLWlnJgL5K);6?9ldQ1@=a&SNx zww*6hxRx!egmxO*6rFkfG|5nY-#GN#hLpU<^^nGYZ4du-KFs4?%Ib?3o_aAN;xWqI zS3Wb8VX_DJQ3>}d8E7aL(lL*LwVm)7SSME=19SwsFdd4iH&sYsAYEAWr~aZ-++Qff z$YCLcn7SQJSMfV2#G-)i<}v%<=shJ?KeryekB;gd+vR$oW5H$!RIJNWL%q)=s4S2I zXoG#EEO)Ve#PE&cI{87jFwL-i1pd!)OiPe2xE_5_zEHs#a6jY=!Ws(67fOcpd^ZXR zKz?gy(4PJ(KW9B?^&&t28WT$9w+blHkfW#o0v8rg3)cz zl0rJ1$J;Ah?PA)|Rcc=$4rl=Q`1e?>w`1(wuKT$?oe=9dkFgP+HpUk6#vC4-=H}SE z{ikEY*pzPSOE2@@#mI}j%H>^^f%jKuoXWsRq-Nm#I3UTJK6&sJeZuHulw>T-PFaqT7E|a zw!6u%65l=!5#fMo{Xew531Ae(@&`QKy*nFnkU$9G-rRu@NVqRCfpA~pl$#_#2;s;k zAp`{^h;kp{7Kj*B6i}39jZqN|K0yT!@V*U-Pf^t1jhIZnU-iuFfxP2;|L@~HJ2SI0 z-PP6A)zwwiRnYYeV+GoSwb}*kAEK69{;XZFcUY&+RaYN<8}a$xc*S}~E6`4K(oSo0 zk=m^10_!!_Hz{^wbieDTSkT*6`}yBNt3~zS3(bljh%CYHE+@Wr)d16#DKY6W88K8vj6EhPhN{4YtRkj7=2%Qs%=wt= zn3@=>f_^|1F(vq&Mu*13?2q*|(3sI5hFC7$TSC)zW3OD9kwjrsT#Y2Uiwub*x}(uf zrQ#}4gAx-G!*v=oWP~6^+NP-g>~YT=Fl7YUjMP}U=YTFRRN^}+F92_!8#(Zh=c%GP zye1s>iTXul#(u(2_7;##BmP)BVB~<`?2E4bMh--)K2crIaEo2m6 z#mlb%bAVzug_BU=lMsqDSIEm7#Ov0JY^LRW@{{)CXQ#9)2(Z29j*aCH9N2CRv6kJg zg#}%D<<(0@;*7ne`v_ibv84kTek-z1170I=uDP<^0HQZEF14l1ZRZQ}e}Iu+6LsYT zW6}HHYCn8hrG00>_~3yZVz2eS+qKY;FHgVx)d#Ps`TKVj@7uBuG+Y5b&(!M}-_vq@ z3zT{`41P&67&jpv@942F9>zOgVREWUK@Kcn=C_o`&G>en@`;B={|%qk>!Uo4sE=qP zP0y6Z}1)JAslt+q1>%VK9btt zROE%oi;-6&ksPHZv66_$(UF;v`H?Flw?-a_bVi7*q zM8qV=q{fVn(U+U8F$ZFtF{fha`aV~R|NScL(673f#hYgx- z>Xsq&cq^b;inl^E8>7>#o|}qjR{wUMa*u~*{|#T&>jTZ|@ms)G|My&=It;^27?2RY zy9WjtM8U+L36S1zf}z&>s{w-zIlEkdB1mynQ&OR7yNnNmc`0>Ea$Z6gCCBFpPyNy$ zW7aP>Pww%N`W~6@6uK&+xj4O#VKo23YQkv#(RET-N*G3^1d-7zs+-j`qvv#;)GVc$ zIWLjdi+pp?$2{O4dt~LGIxm5|3cndVuQr~So4iC#v(b9@VIxI}R}w8%I_zwz2X=Y1 z+JxQb%#$8d(Ms#yHHeS#$}c2nQ{P#XF;dM5+U7OQ30k^NYM#=ZdV!E!71gb3C{P!| zaN#*c=F-<3;IbcLs*(K;{XihBII-$j3VKrp?BE}M?~gHg0BxZ^Gv)8jZS=eHyXSXn zw3Nej{c4#wF2C!DdU5m?W2x&`%lPZ~LgtR6oG|cjo~=s}(!b2y7)n)&dE-_Jk_crU zu68xz(vt%bR=oTLTo$X0bP0VS+yN0LJSc!724F#vnXpihrkj!KTfBFaJ$^-dNPF-{ z_ITN5ZT|OcEF1r$Hm{tQ)n)Vk$aY#*#+TN;%6rzWp-wV0&gu;?n|n7O_0RteW{R$& z!z_dWbEwpI_dLLE`SR-3uUH?~A?@Q!m%jd5`)C*YT#I6RwH)?&8GBJHV5M3L!OIEb zmO|i|;#u|qmUTJWM`I}0Xh;%1iA*3XUU;r)uEz|Iz`#<9)}#|O>MB*#hP&PK_-xA#?33TFngO9 zB4fDa7pHbvhnnC8+(>=6dBw;>>9g-0^7_a7bnpp>Goi~Yf^SF^d~*?-2-Z`>uhx`q zq)aI1C<$alqVDi8Nj7L1ko3$8nIy5rdF@w_F`xtX0-h+tYEzgIsUtnm5w1k5(gcZ2 zw|WL&BbNjj&}LSjoxG{*89r^~nw{=`Y2Uwg`$O%zJ+ScQi~Pm-!ckk+uGu`kC|(Ez z7q+t0Tl?^|zxrh?W^X^6_1YY^^!aJXr2Q42Fn!`=X8(@$?Va}Bb72S14DBBr)tG3A zDc$QYTi?NWC6eDABD=v;oBHT8L1p+L>OI^gz5GH*f$7zCSC~ac4MNaG-#wWWJdLC5 z5KU#QBj`25!rZu^K}9xB;DT4JFREJi$d1xIl-{tWZn2{{m!+o_t&|vuieeV+xdTtY z=c9OA*^0aGSX=h)*@A~VwRNr>Fd~D!`*hapbJjdLMf*d$B=$ch``4-dlI05cS|VjE zLd-YAWiJW~dXzb{P3@p}m~cvl;qBxa9gg>On19dW zljbe9u!uGY=Yu-h?wR>@|zU{)+7k&ER zp5+HxPa9vNc8WEXCuJO;sQ zDI@K(SI7wpBjT0F7ED=B9Y6_c8$na)rZW%Uu*7I>KfK~m+|EfQFDg>|oNQN%w zzUZL6;d--|rQQ5#{CPHI#C)*xh&EM{ogY7Z!VeKDFK91m=MS@Ye`2rK+0mWn4o*9j zb^Ec~e$}qa&M@T&#%2Y^rnOQjBiR_P#6HR;$<0maL!L-5$4Uy7bb}KKUhYz^3<~{t z3`Ipv%X&m3aHY3|q*_iz4HYRZA#jzpEN=-x%z2RQaY)!-Hi zYAUMm5(UihO6f<7@lA#r1OT@zl$M{_l#y$(9vCbH>%ox|es%p2Af*Zs1dSm+m&2An zCs{wR!V*YVO$uz9ccmC>$}!OzGPg4{$uylvvCAB8A9J&thtRSh<#^!7RgT2ou6imQ zD)u+$)oPVg)@r&63>;HQ6jf_*UExqm=n-Cp(i+d{ODL_?6nei(J+A_p8B|Xa3Yrt% z`VJ&?@nTzYPUvXp;z5!Z{iJG55Y*}WGi>Sz;Wf*+ zj(X7z<&!LjPBPNW<0o}_CXtx(3JEA)@exQn>BUH4!h`-1sm;Lh^>7J1j80X0NiG)t zY`vmA&(fGu;2MyUwspd>^XHGP!+^GI_Ci7kmbWWm2w@kX@{2!1_(QRfM~j##@36T@B=6^$hB-@lGwhbL-T?EuO5GSAKRniLYg zLG_m{&toq5!e`>}{|28Sr$QWHhqe2~Mg$+7GE%PnttD0davb1^2lno*dj{Z@tsik{ zbk5EpAAV!<D>JpL5;!%1Pk+FmhJ8gR*cK5p6#Hvwixg#+q4GI&2bNQE-z{yCqD*GlYA8(IOiWk^+1E%iy=DgJD>3jGz zb_F^BBj30jOxXs~ChBc;fDZFhsmq%p+em}XL66Pbl-g)o)3m;+p!nZN#+THTa`9y} zbu=w$YR2Z(+iL?D_4bPS!=64#a+dAs$|b<3w}*J+kYjWlw8f5yVgW-BQ%O38`sT8| z=Jw{KWk_$H(cICzq`7Z7ev>=P~iWYN3p=Kjt&` znHKozp?~wVV?JY_X@QQEXhqrtUq$;d)NeM!a+!@)3||*1$myyiSqItT)HQe)^dsM0 ziZ`KG4ngQD44YYBjO{10KCxYS5_oT>^E^pQsLyqg{6fgZNrw&V%@gV|qh3{P?;B25 zq|QmMl+KvG$()|Jk;HB73gZdik8ofRn!(;GsJ3C78Xir|KJTvs?6 zmf#mUy<#?L;L5T@C4Wo8l`3`)`t{$m+1cIZF69L4{{N~?9gRkt7@wcrZSETOyLX!s zg%XDvXoO-A(U>do81HAfY*I zx2X=50Xc47_g2&mdgz9_yI~L$ed%F|8|HhJpV`R2!K}aThS}E14fDNs*oZQ{VRq%i zMC}}b7ED_9i*?t(NBMz``CI)r;^dOM{yldDlZGDk+sW815p$j_@{+72c68j!@iy%K z81M09_y4cP+pzy*ykY-$kN5w@51>MO)`Bx3=mYHRSc5VxuPOe(#VpX*4{~jSO{YOm;6b$%df?|s6)PcC-j%ZpCa7+Nw06byS_=vc9vz*ymU44 z@H@S}P@_JTO&SpaEwpaR$R`6wW^_IoI3i%%NDbH;eIVKyeJc7ww3TAIqfgYQXQ+9ZDl;Dz|0UiS{=-v7u49|0fyK`nRJ zy%8d@uxjkZg9~gGh@=d=PxLA~?j3>k3Cm34U>eDJ?+)58b#ar?#H=B|-{c(>`E z-;x$3A4Ho9@xglyJ_wpLX~N`91|O7ks@n9ri4TrxCFjLX)DWel29S|WH^f=N45jhk z;}T%h0lyqBBuyvkbSm;aG%a;K9va+>H$GPvp*^zSfd0ks`Z`I|77tBp&oJFrANA4P z5w|N1fcstAv&;c~bR6aTkk2Q}xmKVZCV3jqJL}J_jq3lz`&rr{#6KfGL0p~Y=KlI? zc&^(!KhxhQ7_1Fgj)@=Beq;1a+DfI2fI8@1%Pzc0a}eoJjCZKMzJ9cppnYb;ym%eb zogpJgZKRYDh(0K^kmLhY?^!7$P`#KJlfr3CBS1%(+@AM3(_X>$o4f8E%4d+njJi$R zvFjpz6Km8BKhG)xrn$A)x3q z<$yMTyNwqNI*$DYyX)Sie8h&EbxWO-#0h=lgOdcg-s^Sm%9iWB3Ag(_Io`JEdff`q z`Dd&UW0OhaZjPgg8x!9|3v#_j-9+cP`g#wFkn4TK#fYmBwBG;Sf-c7kXLYRgHmn~E zr!{F$!7j4FkcC{qv=5`rSZY93yYua;+tswIZ)XK7X@?~YIg4PdYnRfF7gm6laVc@6#fk@)D~T_SCys|}*Lix79G^-LuHxDUCh%kf zrZnXkqGW*ft6=vw-qB&eT+;D^xfJM`OQxOOyFQvrdVOAVDbC%dUSEHAeUsp?k#3Hu zG*)YJpz?~!>--vkeCfs;C;KG4Waf8%ExS)sSl7UJ2_Se zu>rt_Ck}F6G`8SXjg76L{1i3R{ZyQ7Fow@_xb*8q(0_~*oCg88&>nuNuXl0O?^06|m zODf^;dA`p2oBqtH%$f5qpINoJ;s9!kR}cxUp2IJqKU3hLn%Y2NAuNI=BXe@a0p^6C zIR=L*qeU$n1G}*mR1Uvyn0OJqDlB^Rfvj83SSr|TZMO@x(R+2C_bikK6jn7jDU6{P ztrN9J*u>NFJ-Bs`QU28HqxZ=AX1>=#`-a(ml4IH)^(CU+!3dtXTv1N(R5XKr0v^5( z&*MZ_Jg|wbSf%ynb^Jm3yqkR94Xu#fdvtvxzM2vI0f^W|(c$Rin%=0@t7+Di0L|2n zvw{D32Ia^T_#FMhEVsm1DQ}1o3)#oMFKu3?BK776{1L6?Z(3hN!w*$289QL#DmbGR zvkhlhDIF-r(R@~{;gA%yKN0zoDRxDuvO?Fh_!!`Pn*6)thItCXt=1>yv}thkR;ZGR z+t16KkK7ybs3Hs%h%Vv`1PnK4IL+!>I8tZ9f-8vA54E}*4#h%?3npOp9QJyG|_C!(|0EdnL?^1AID z;!LFzVaEr1Jw&R~QJ!1Sg~W#p9tFn%@94MsFUGTOzR5bRdK_Fb$U~{^`&(!{3m07u zt{b*EHg@r_^$#ChH@rADwrJ>vgLOd%*KIg>aNYWc#p*G`hm9UPY}jb_>X_j}M~}hP zUjuP9hMrwp$X{8=vR6Ej(!G1i<0Ye39_`zsN8h6>#a$&&Jic;d$XWc<|2Wx?*0pn7>kzco- z06*yL&L!t_9!ySez~F6Bq)ZKT?wA>bg_$f#g$_+KZjZ?uv~+F7;C|hPgjQG|yFGRA zz4swC_9t_uc2BB1DNjXB0T1!ViNHl$%6uE2L_wel6LK<{ zCLGh(&D+$Bqp+?6ByW>l!BtIH5Wle@psRGcT1f>Y@B(K%LTh_5CnP?PmuO6hCJjZj zJsRi5qJ2_uAi)uypwBc#y+Tz31*&R`v_*C!VAO;>lM(weS~K%$!wZWse|xJ1b+)j@#XR-@d@d=aiL}ZGZjSw~ngC zg}YX7#~tv*m4lEYLqKcM@XRSuoXvIg__*arsn5ifPi_+Pc$_l|^mGB`@ldaGd@vy5 zD5;;9F9LkDV!RLqkNHH;E&ARF6sO0`rh}!#aT`VB5V9VNt20jZc4>rw7%~`Nw6)h8 zZ=`f>)Gnk&(+TZw-?Fwz?yAfC&#;-7SLHTYTl?Go9r(8&M@j$MUgsEbtDm~Iarm|} zzHQg+Wpy|weP{MgX06+LXz4NvAB#CRQ~Ta>0_!u$9S_v6!Tg4=E9fU3^8S0Ep9E#8 zzP|Yw|KwsvcKC!i(cYQhyOhGu#M?_t387I>parWUIL;miqKFGQ9`8W_g0SKVMKp&g zFw*lJh{17N^ZY3ph(Q$cKT5aYu@i$X!Xm|XttGcSwxd^m*U+BJ`hKHb+Kzv$fi+rzaU0Lw2EIK5(JS6nZqm9beq3hs1l;`kn z^u0B*ll4;iD%p~kL?$_VCpmA?3B|{ov@M*u+R~zg<3ORFRfF8sT6o>IMfeJ@TO_CH z?ztr<&{Ph+9=O6y_T&tSq4!;8a733L8VidRypTW4ExT9TwQujr6?-gv!=TiDg9i6c z8ORzQT)*z&gKIZDynW1wp`*tR9X3X@5ZkmwsJYvYmW(WUVrP!pYgF#Ak;8IEcdr{* z^0-_{j;?es7k?NGmg8L54`LDU(h}4HUqvHhrd*QEY2ub>aQLx-*9-uoLwtI;D|mST zP{x7?uXn(MN$q2`sqp#2H0XwN)|6(7f(|#~Ht7{=^tcfiCVx|X<&93+U3+AAEZ?}a z?6qT=iz5+7q*u6=UH5z+4p@dl9tZxcFkt6Q4WP(owL(_rWvxak~kFe<@`I!vX zRReRI%h4EHiE=73kRH{QO3zl39S&i>;X^FIDK5|=;0Y{>xj;5F7LTNKvF)~ffClE=UEzBEoKm^e$ z-HZe%k&X28;>hf7gSSvzoTcf+lNlewoh|L`c@|Np^w>Ny#)ted8_UlXY}a9 zhF<=XrEBNDNz$I_*6WsAMMA$`+7;HOYhTHq=GXtGjSH1vuN&VP$6z;8WI>hq}JCoyqB_s0*jO%({GhPkUl+ z_oTT!`hv4(h;y1*SAP9(w&vDjsN+bz#<$42KtK#|SsHul@^OalfVw(RT>~#TJA^pL zm~~Mf3uRqTqK|rAu%%O7V!o#?AD$!GG1b+X*rxMVSr^882y{Bh)*rB!j>PFm^2s&= zznEtOy&Z-Sz2yd9SqsiDHlc)=h^Rw67LFJ3p~Ptr8B+RXFzf_bf9>K2A8X%U|2X4t zchU5UbNlKeF-J?*tvzV{Aw^rDee~&X+F91@{I@K;Pgm=U-*^4%mam?;D{b1@`|deQ zYs=yK4i?HsNVx&bY|e7WMgxd2Em%_Fh{e5E0z_t!`y9)!XK)_cGU6TU?cfF?9}W*>--GpWjhtt;>;Z9_H)B73}K;q6sGf&ai#Wok;d1NxB(d9 zGH-btFa}WUe8VZw6bYqd=62Jh>^Nq&9?Nu!D(dTqgvc}yOp#KtTO?0ttcJ4Evvr^B zx^|KcVdKY*vrM}_np1oQKsFcqtmiQI^?jC+(@C;xbM{MaW!{*qz#}q)zPyvC6T|kF zV=%q2?dlBDV@+_>)$}AcNJj?R4U=1-#yz^l(OKBAND^)&hH>b(ssR$0&9vwXw5ZCby(e#JfPuP?N>KDe*! zLzeVK|BI~Chh_Ud(%!7@|LE&1d_lrI7JmNFq4V061qpfDl{bz6=k+JG;e3bM0qa#q z1(Ba+zhoz||Mv0xhqt9yAP_7;zNhl_y`N2=0xbXNUVE3;4IB1r+-G3rgOLLhnl(vm zK5^<~_wjUVtH+-Y@ZV-t7vdxf$(q?L#L@&?n&$BF&sSVcTSI1o{42cIK9i#o_k|!0 zG7a(i8AlU<>1g*$xQQc4`gnrZOW3m}d)-3f>VlyO#W-|t8t#tcLE{t`X9I(_{mWk* zNz3~UOwGQnfB)3w$qka1r}poETXyQee#?_W^l)5Rk=hpXVUKc1d~CUd{ZdcZCjx;H$jshdMxcpc z?D6TiDMd4ZP#NIWIU5ljqH5b{*2ZJU5ZMEBE;L*;e^L8t6FXd2w|e62S6I|0Z7grF zW1{%vVC})Hw^X2Hez%a^dq4I{-@x}SUQDU4tt)|@#EugPCsUPP`y}7QUP3mZHUtk5(RC*2sIShGm;_ksGv|*?CITX zyH>DE%i}+2^^MfV*BShL8^bL(zOEhrfGeFlYl_A?lRlFv+4W0VDr}!toVW!)bty1= zR-{@+TOhhiR!T}2Ed912qOFxp)qSW=glAAR(_=hIeA zit2WI%Ho_4KH$TU*Yfc8#g(tx+Z+gMGk3w_?X{CIel(V_h*;v$k2qy0`ejqn`Ag<= zb&uz{nCVNF*KaeqxVDbEn7~DV3aVP)w&ZNJ>FB>Q`7`yjmfP5a+fF)T^Broa;lJyJ&LjN?9YoVQ3@T zAQ=ff)o7;c8qHm!5n!fA4`atjss^x4OcAIQ5S=|bEEcL`q;d(P1sUGhQ2LWRr2i*{ zBI7z#e<50)7o=S{O|)2JM^4wG*UIdT(Msktg2-@S9&9)F%LIBjkFisRZs*6pIE&cY5tl1@fOQ$|3);cW2&l$ISf9Wy>Jmf4~)|)xBr?z;7lbtTxy;#$i4y4UldS$LgetCQ1#b zSe?eOsA^bvG*0_~3DJL+W}>RLU+n-(aErD>W*phE?vN1wV13$m@6#nAz0bLG=Z0G> z++tD3)^@7xB+mcTt#6C?yGO0OchxGjOV^ZcokzCnF=f)!N9#sd{5M-*i-NBTpJ#oV z$BDII-8jQt-v(R<_HVo4s{)=p*W%!>W8Y4;7Q_3`y@6#!P@!GIM09MAE{J-^cFlR~ zpJTOm9$YZ?q-7K96{fwo`Q_KwgNF$PdmdpEiQw(g@RmfpeqtKZ*Uw}cQtHN%>FUrx zMw{=DbiAcU$3ap}q}MGX)W>^nC1KOKJe|A^r*A@|&}mxgJiM=Y&Cpwi-uCRK?WZP> z=s)cE+EqulPfHz?nK7i_lorE#^i3agOSchxbf2j$Tg^(zU%os)Wp=BUQ~KmCTBId) zN$cOWYu~VMF7TLRS=pAL9OD8@1YE9LNKUkv&CM|8bW=`(PLd*VE;X9i(edx}4U37_GuFIafW>t|FP`&_;6%9W*}ZS6Jo7uN*jp(()U zL%?T(vRbEksUa{yaPP!{JFl}4q$h!0%J+1GD?yE-RW1QeeF-%QaO$%sB;k+iSOP?> z@`PgvurxaoN)mYC@phhFtE)-71pTOl^wu>}H)B5)t*49fu>UAMT%pOUyMd@@<;@#k z_#9WiJap=_%<-8yS5F=O$;waMk+EQfBkwTZpD}*K+|0r1rYDXb7`-|!dc&*-Dj$1L z9htXq?97ba7zYMjpCT4XdK#toU!GYr`)>+uKF3x01((24By<&fE3f@4ZfSdiH9Z`R zClVWPlV^JO1%U>|qS{~B?l}XeP0TJl$<>R`9Xq@D{%e8uxOlGdhVIS z3-=!QQw5ATg~46|j8>d_kbFsMgl>emOdq9ldv!__B*juQ#*#>j5 z8`|`v^FV|JxjQo3^Z~$}lh`*<^{EZAA=$20b}7(iD72+>O@$y?xlC-t=^4n2^KeH$ zs23`isTnXt3NLi`FCke;IpIna3 z8>yi&ZzSZj2_>AHXRb&}2DZ}Faq29>R#zaZqCmZi4l{1WHEGZ=#R4x>Y!2KXh*(?= zMg&d^ToAZC&_dvcA+&KQ6{JN%0EjN^753TFpRp(3(LS#HLcR2q_VGK+Pgup>wacjd zYmt~(dj!CvAMwER-MDO^r0V^U*fgQ_>4#gR#V=G0;Nv;LF_RbaReU>_a2T2_AhA!l z8qWH?qjh{%OL~X38#`Le`D+ncfsaCcjipH9&6wAc-Q2W2b6mYGtmthc8HP&6$CXBt zMwTM&+v?A>%cqa5TaRAVv6nvm^d(+j$Cho{xSZGP^n|qsF}F8Emcm&S!oBQ`*{(Uq zNKffC&7i12gN8$fcuOlgqa6?%#ILo1;#UZ2gum(NPb2V?c;qUS_4+Jl((!?)F}yWt z<13DYKO2rkqIq~b{Ba^kzK_nxm*YVF6YE(r00*RbIyOfVl-rtw(5{Ar6=UuR#E9h& zR=@Vkn{UIZ>A8EB+*z{7^}*}f$LF*kRd({B;=8x9NTAp;NjSQctTWzXDQ3AbOu7 z`GB_o;__E0JsELRUpWK}!4ua2Vn=wG87Tmn%Z#jLGtRI#-uPlx=G>_PL9Yd>b7#Uy z!B3vFzs9PyNXzDm2PdzHZ?iSGxWjEtL&IlwEt)cW-;7Dqa;ZQ0$V2Y5e^X}0|T;wOSQkTPFcok+C|D>Zvk|cGdnhI+_7uprfs}E z@QB?)KY(YmzvawD2*6>$Vi1u5+n4ML$YCEvE9?Lzd084|4&@ z3p`^xYz?7awgxYbL+l2&hS0haewW+UfVpU$fIbB)ck6wU?8=;mKEkWJ8#E^+_cqrW z)5K5aN#%ko7LyZp2}REa0AIv3Y>`MGNQTUcQd_xAE;F`iwgtB3Hp_HZdmAtyqtPQ6 z5~gcBmP^@P{)ftD{tijIVx9JcdPZYHY`_ZBj@P~?`tsT{033ZrAH;lIT1~B|&pKoB zUvK8Ci!q2iJc9?-bv(0Y55CsNw;!$=QvNI>PP0!{K1-HauN-1?wRM%!@+#vAK%qr} z1gfdPl{>sZFkP3c#}HJ&Wv3sj@QEH*0Jk^B6>tSG$_Ik6Bde|+ESWJ|G<$ItMM=Kr z8v*O>W$_dHRSUFihRaW_weJ18LvX}m@SUp>OVFPX6E8mNq1&U!H*CR;S|I-9=}X66 z`cma5i|0=Q_1C`l=vmjJYIgP=uvkc(vFT_-C`PPUANnz;ffSj$u<55y<5fwgv<6JU zO@NZ8EC)3;`(GxMtD5xSlvat<;?wCC$Ly%cr`uE%r2X}>=}ACn(WL+tT`W^xt~-Z` z)cs|4MZ@l~?hg(ZTJZB4%W&|Y2#WV&*H*=;gNSFoJ~L5Nu18EI$*@(#yizk$CS zW^l=tJgcqF+Iw`v6txWR@bgpOAenR27oY!P;U~AP*yM6<&s}dvh^sUE`z<8?^bYes zyI;-DduHRWTb@a1d7WZk(mXf^oHjsaA`eb|DA*)gnMX?^NTQ0c<^A=GPoYRW0( zE-Dt)T{$B<)$QcS*Q^CtHt*G7wba{wRgULvH{p#Y>u?2FWOkc?B-b${*YKW_3HJ()-8Lob>+j+@iDLO)X?tzbTD$v$Ww>t?o@2Fj?67 zcQr&D-Li7o!Gp_6wy-OEtf!ZiE+C6U3*3vdEPJ3!T%Q$2_$`dZUa+jz%SoECtSnrYgW?C#yz1iqr;l z#5}2CN<&Mb)7_XyX+Ub7CM4xY$M@)B)m^qsNxB>RWy!1jN#@|{!Ht^^+W5T#hYuZO zeYr~gM4en%I9ctIlF|h*(jIC9#>@szIbEMwH=WQP_DO=W!Yy(rk}67-axA?*Y84AS zh_DR%m;3YSmDBte_%HXDJJm?Jy@M$b8@-8|$a$T;cY?k9THX6>?TZNarTuEz{MRD( z2>Y3Ece~{)OM$uvd;CD!-C?zqUX6t9$|P%wH66LZyl?`^2ZmXcp4{i+#IdqS%vI_C8gEMJL{p0?q|!7pG6Ii<~4 zhd`u=bO8wb%u%D?8@O)*%qt9z*|HTncQKN`a_G>@oJHKPt=;k3+m`QS%WNWpQbfr^ zpF62YYIwR| zhM&W)#IMw^-0zqlB@C?gtMLmcAW_K!ts7HbzF%%4_3A-5!iSS$xQ9N$Z0OCSr%xZ@ zEb!%DS^plxZ&gdy5AVg$sS_tu#_n8gX%P_>F=OKJzNxmpdjEjeTJ*0K@~hmef7Bn6 z-OS!t%)WYcD%iJEwgRs)4>s}cgr&Z*wA#C!#VROF2*=wY&gSN8dce>E^5Abp6UiK_ z9#Zd5ULv~VFR>1HZzK0tX7%q@5NGc)I!!}s2IYar0;>Yg2U-fj1_Cnz9f2i*U<2+^aoNeFUylN8pn<(XQiGHR zU<1MAkKt@$cEB@Yfc^#!5Z@&n0ewI8VF1(1c=nc^a#q2 z4cha-8eqa&9ccYup!IkZx~a<9%-sX`VBDk!Ah`H$-8~e?|H0M!*Q~wY#6(YQa+N$NaQT@C1utDLa_wf_{j`}Y87!BLD zPaiAwArL^e*;ydlGZ-`umHIFUL^Q*lF%qHfez*yZ{QTe~1Da~>{t=Jdrex_pL)3P| zc!l1C==f_~J+N;512(=hZP?&}J2M9NUfwRM!=xEQJFIOVk&G@+Eh?I-cJ0@%D+?Sx ze?-^Vuwjl7UEfQ$&!(W3oa&k2xCnqnL8ElV?b1O#O-B_XF$&?y;%Yc2qkpI#Ft8K<4I`m$D zp#DvMPUNFb!0DT;F}af)K0i1xPPmkw|0x=31_tTS&ikYWeYqcQSVf&rLoX^w7@xX*>`E1 zxUpVu+1aiMZzV)oE zP?SxFtAUTr4xR`8(zn_>xK>yKbrRA2oDml&#-CRzDA1%Kmi^s=1PRrx7^GzfEBDbY z#cS?fv=N=W{^~n_6&9{9&}!a!Mf=MtPHtYdX79S~M@5%a#doe+o3B3n%?Gbcc`~W} zGlj2w^}#dhqIK(5uU&~@0Q=IoaE<(KpXOwv9pb0j-uyWR-px-A z8bWxaGuwkCy~mptcsY4Y@c)Hj*VV9zq=$wu$a^Gr=YO*A>hMc-ps#(`HGBuS&-@|x zRi!?h@zUi#-h3b7OM4#7-V*!h$~)F>v!01+ac^_`^|RqUk<{$`m)a$SW^V!IU^6zY z&M|gC@!5~x*t7Kn#shXGfiuF8I1}~x>*M9@y|?nRB4Xq@qIpF3XxuoI)L(D@@@QIrY4f6jEtal@k0m6YTz2lg_s%VUF0tLw z!c%X(Rl9FV+1@*rZ{8wWFW9oaz_EsW&M3C@6-$UgE5m&K8O%tw+mDF)FHwNBt>*JkvOnK)Tvz`2k*|bal99f>yrcX)X!fg>dHZRz-dG*I1 z^dCHwS^xP7Yo=Y&u4oUVywJMMz4qp;ZpP3RRi|IPclQaMe~@m;jy)5_8ixn= z?E6V|qmZc4zhMIe?+rJ#uoN`>h-rjan)Ga z^UG!x7do=%%xcE2@m&YCbIoJ9IQOjU?FaU&4aemy&zpZ|uWq1;+u_4wF??JM4<5-s zZgg;=Ex*mj9}#dd2*Y25Iw3oBoRZP8QctYn}Q#-4(Hmxigou9i_#ky5y>C21MLg=$XAl;{v zHL!vt+$HaE&BJr64q;3K} zeSL6l#HZ6&#`oU;zy~K9J~*46C}ACV>6&nZU$sGFXPLjR*PPcy7^C z-K)@Wn)NlkiZ;*PyZ5;hd-j}seDbJKlP0H+oWxqLyZ`=mSJoUju!fDe?Y0r$kGkzP zxER4F4Qs#w=z_^d(4ZRWAZTiXZ?>SiY)xc{}`|{&3?<`sgssBUp;Hi%A&P9^A>mRdj8lWU;O!!cI2tATQu9% zB8)vWtb32;hqh0fL!trbY7+W!7X8rWA)IU^?jz+PvnMx^hmJuWs<3xR>HzJdmxS;? ziACi3_}_>{;*mMG&AH=`Xa7;ZY~Iwl-&P#G^w7qv+-=+D&s-P1WbE{1cTOKyB&JQg zCpLcTj0Y<#ADpo*F7}>@dk-J3yKTI~F@EC0g?#p~nKMR>oHdK)Jo$CxU=8<`H_bkq z9G7(YzVfEZ8-cNgG^S%FBIo&s&U~|_ZmWn8zh2f(X+J#sEJ9}Q-U}gx+Q2;EN5R(v zP2UkI)0!0P(}sscoeJ@9cnhPMuCf_^bdBR^*<>vf-cI-5|5{~9$x|$R+}&sb=M(O- z9ONJ3+&{8J(tZ8!kdI&^fgkWDfIx8oX4l_|_FZZ$|b;8Y$6IIF3!2LgmDx=Ny=lx`g;%riK8U7xt?DrUFi z^u;ixqAK`&FbwnM!T73%iXAks0K3AP6(1*Zh32O|KOod5wy zUU2#L>zw($GH+Z9=sqaB9Mor}LFGZmf~taW+Q2S>z|)G1AO~fDE2Re#G=ilfBgBEg zkrGUN7g8Q_ETk%gPAL%F4g%ROK_(iLxKxLlxMa5}^-2yRwz!hQ@N#e%UQVa0YPzbT zqMFhQq*JN^Bp84doNj|rpcn4?zBKC-kF$pGy71On(L8MNDQ&KBrZoZ+LehG^HpBw^ z8@?k$vrSpb7S{iT`p7o=s-&BZ2HkAnj%h}58e2ZOeMwTtPg?y z(wA5Wf!-7&`ylI=G(+`+W(NFcn$cmRF_+&c^X&h>k9qymfNr|P=~^A9_1OkaE$1!O z7MhC|8geS=h=Hq$u9B!IrCW!2>_8tM-p@)nrMgt5xV4T^+j%&PxXNwxAr+-?6>&LG zRFb8n-cMrG&+eDxm*SW1hv{c0%YPLeje|FUoP+h}z+;moJ|&!NY`qN|hRn8=9x%HT}%1SfIN)_R0MTXU3rI|zz98^$! z6=db1nS`b=lMG`I%_M5uL2VNN=KRH2T8dSg|925yk-Km^bRYjE2LLXm|CF@`->=li z1w1lA>8H3(wW8(_U$3&N)%jM{tuR7%+__4rxQe1eX%#|A*$&{v)S%>6sjWu0%50V2 zYGo_ch?%R`eZ^fj@ys}AY+a>x)N!7QQ^?%ts*1psor-EI98?^@52!UFHDYu`W<-9( z$_QDjK5xnAOARyXB|e#exIC^>cQvv?S$Knt(l+>lAWT zx*V;k0xC+WxJpGnen@Q)(mJAba_iLA7}|Wof`nIML3V^NgO?1r@e*}K=XcU~)r#1Z zVyQ7^pTLVe0qa`h^Np(;(_Esc5O#_RQAJltRFoQlBP5;c{;w2s)2O4E?SjnKA6O)Q zTTblvykx($G$&y>cgN|k)Wh_{bW%mDd^NoQ5dghJA@L#Mqj;lw-H3O%3-E;_cBc87 ztJM6;q+`8LhzTgaL2kqRme51Gfx~RZVHm81jh;oG#;Fn{4T3rSU43_$y|E5Up@9`cA99lnKsDae$V%B&>Or zl=ZYeNb-*$dl2P4O%I~w8^5`#-3uYDk!FA5(Z&XOeu`udnd(B$D%@_BBH!2+ZuHs${BSk%4d$nNxVx~*#y=TR69a(T}6CLR|o=Xx=tZU zrYpn_uPn(UkTVm_QBg$&&Bh9vff(kX2z#EIjXO*bbd}>V++!J$6x*~$6sH!$1naDqmMDx z>oV7C8}QadX`%E0tq|!+K}-@M3L<}x&{pLp&8tMIAxPS5(&t`nw19_<4j<7IVkQ#q zbhv2l>Tr#ls`t~B;H2a!1vsdn(q4PKK*30Rz=(!`ygSnNR#9KNf@$L3QB@r*u)6{gH4; z+S0$7069yiEu#JqigqfeKu8qP7Eywn48EjXaWU#@l%y>$LMGb6M4^UgzQOko{(d@a zIO{))P1eeEyjiA5xdFkR2oEJvYLLRZoYK`K2$QI6AP;-12Ou*{lxz1|+0RM!EzGrB zp8QL(rQ1k)Cou-H3_dpn8XfmKpKSnGfRF;-B!v_M zC%0&$*=v%P@V=t5iPv;Ki|~6a*DVvNY(kArwN|Qy3P?e^C`xz$_CKISk?*WUa<1lV3p7}3iVjH5ac(^9!5!U)58c8C1HLL_2f2! zmNmJ+IdCi2s=aX7haWHMTupoxP+1jn zKBPJX0*ChwPv;|Av;`i(fERU3ye>2Q0eO}9i!QH@Q=TwngPU>~_Zl~qVmphKax4m# zghM|s#ZkCRxdl&aVdd6V2UQHXGJ9hfTl2z^I<uTpH?g>a7y9z6fq{r^QvR1e!7*6FTlh2 z%$yP!CrhHl|D6OM@E;5OL-sC@C)~C@-?q9fa9hTdy=vhG#x>j8#nVJ-cM}Hi(^R|)n(;p%x__D*a^KCoy~`cPlW8QA2Iul z`o}9TBN}yc$hagDlc;XU%dQAq>tmzCG|c_YZLF0tSL$U*=|cJzT?ZqzF;WLpO^a(f zEv`vaRMBlP3inw1t%5Gt7#FeJQXpZ2!drYOX>hSlgS~YcB%_0!ic%^ZDD-;d`*Izj z_rcE)Lq=ao;1}*HrJ~v>XweHO4>%T36+p`yJ#|oVH2~m+1bE~hy-w00$T~6iqK&yn z@_tpr^9`#T(%d7H2*FfYjigca4dvW39C_0l(%hpLZh%+cUz|nVw7p&$`Ic!`9Az~MK-qXK^;OpNS*OW7Akb*P$ zmInCcRu*bpGbNCXF|N4+GBB=%(v{6Ot}RL{R&HEdl|;m<*6UI6{2SxirlfLVTsJ^0 zzBJ=HKxx9$jq5-qj?Xf#gOq7}y>Z=88PESY$dOkRF*~a$D`J)-|Bk}koVi62gB^~X z1=$gulRI@D>zL^%a*WN+S-c>tP=DTAe->dp?2XsD_&&NVyKqsiBQK&;$4<$~ow|1F zLsc2?bTB?MzQ|n^krh!?m^C|lVOHV%2*(_w@s4_1df9Q7V`19d!rVngxmkG;_%ge& zXptjtMDDEYyhYiwBNpe)&Mu57nwuTbFF$J*{usX`MwpOv?wCBcs3^a8QqqzoOFCxB zw>mlsbCMS5bu3C6F*I%9sBr^3;7tse0|$bNKmxOIG_(l+MPQmap!&T-DMaX_9565X zbugYeaJN9o#$9J68G*Pu!>!K&M_vcsaNs%{zc0ocS@_cX{oX$BMcnXaZ=<#@|Msoh zPD`l#K6k#q5Y`sLJTezwQ@uqfvm~r~n*@Imo)i2d@Y^i3m52Lmv>E}t<;m6y zB`kCCGy+0QK5CeS8tKmWn?yXh5htC2lVmwwMfkEej5JBg68XQ5sK--RN5D{su}Z>N zdBd>?Pevf5RvH{0Mk(V^cEH$Bov^LSfA#YKG3w-V(5dQ`Q68r#Arq2!Nd} z2z((JQg$PFtA;{569zsM4v+rk;8QKZ6AR9$r_#CC+Ohi9u-qK(d7zm;s3_r%9uq+Knheo0^ z>B!7E2Ba_!9B={R&?pAgvKV#k8tlX=tgG~Fm@)J1Zqsni}ugdSrgUD)fR(S=o_)HAiR$%Hi z4BV@TwSQiDU3o)!6P$7mA~;`A-a;On9ObIAO?h8=S9uRZc18I?Sr3k!3&LB7VaZbt zV2}zhXhigjK{&-2$nU_XmttZrQ|?rjE6*qoC?)V#U8&rqe6Re7Jc%mSbwAdC`Lh6} z4k6JZsC^;pA|8mdHA=jx32KD<3d;g217(D?@M))`RtAy;yJ7huy;ZvRhe-@}+W# zrLumkKav;^U<278HW;BVhq7U8I2*x6GP24aXX$J-8-tzaI5r;ps@tG9nWX%s)GA*o z|HNKl3Y*HNA@A4>mccSv7MsatvDqw}&0#rgF3V-}*nGBtEo6DhbIif=Sph3lzD9<_ zMam)$DF|4_nRF zu(fO*ThBJIjcgO!Od;>tR<@08XFJ$Vwu|j%_p&`~FWZN}<@d4u$P`%44zLHJIS7BFR&_h zioK|mv460a*lBi#y^JUiud=i39D9wu&fZ{evh(Z$dyBoz-eK>u_t^XF1NI^Nh<(gH zVV|<9KEt6^8!PwZ#*3;UJ*#(rmi zuxsp3cAfpjYFQoAkj6yeaMa|2TR0M?a2xmI4Y)rK;DJ1dH{`)Qgg4@iaoVp5593Xd zjiMQE&Rg)7ycKWF+i*LN;E_CvNAnmS%j0-FZ_C^91m2z}@(#QsPeN?gPP{Yk!n^Wr zygTo~d-7hqH}Au5;eGk7JcXz7e!M?V;{*6WK8O$IL-0U-jDD@{4hVlALWnn zqx^CH1V6@|{5W^bidXWd`7``keu6*8Px9ya3%rV-;xFR(#!LJ(Kf_<ord z?0tpulyU~AR{o)^6;qI%;$<;aOoQF6N=#QyiWwq9`9oxiEHP8e60=3Nn4@eGIbyEJ z74yV=$hiBU4XS}obUSiBA5u!O``e{FD;6lH5Z4(QEpT$IKop8aqDU+j#bSxLUECp- zie+NCxKpeUC1RzxORN%ii+jXsu|}*F>%@9Q6WJ&>iOph*D8)&%ZDPCFA$E#gVz;=Ap#KE%|yPwW@>i*lUndO$oV9ufz|!{QNfNE{YN#G~Rdaa24mo)E``QydpA@ua8_ zPl-zLw0K55D^7^##7XhIctKQ&Q{qMO5Al*XEzXFS#Vg`faaNoYuZh>i8{$oIUR)4w ziMPc&;$88ccwc-VJ`^8`kHshAQ}LPjTvUsT;tTPmxFo(3{}f+~Z^UKst@sX6lfM@~ zh#y4_&ei=SeipxoU&U|Yckzd~CjJ!H#b2UU)Co=0TNDdJEIeVcSgaP+Vzc;J8d&@- z0hT~Zkfos|*b)M5LSxIQ2_r_>7U$(AC-)nmF3g%$=*VlBpIw;im<@ThD7!FwwymH3 z*p{XLRr}2>%r4GWv*aIJKSz!uFMGZ%OaB{?HY>Mq*5ZY87G&QZFw1i-+vt?s&-mM4 z9WW~kZK<>MKLgNkRuS&#k8PmQBIIxaNFAuRm@WVK51j2N%9=&s`DeQ?Z37KBvh}~} zK)vE@`6pno2asRxwF&}S+f=wWvdJ1pTH5`?=A4UQAg-lS|I;eMgR)S0u<^f{rf!q z`%zw>1m=0&sH61n^K>ldW#u~-6%{)2=Vpt6c{w6GFUOW{;K55R#rofXi5>_8mUvxH_PV~qbFEI%`?^&A@t@+RA^)ZBOX~thUd|$G`dmk0 zp4B1C39?*FrB3&QlKuM;ta>$B?n_(0f%1_pD_i~zNME!dYtdZ&+Tpnl9Os3=z(rm+ zvi{Cpdgy=qr|EwOb!qVbw0Gv=Q59+1KUKYuy%UyzAP5+61=Ar5L39ixQ53}u89>KD zL1AQ*Ra|gi#u0LWY23BWMQIbbyp=AG*5w{FZpIkT*<+|BJcZ*X&KT3i zXmgL^gbQt+OUEO*l{(LPW2R0WLt&hB)z~re{F(BCnevYlC=yz~yl{f+;_*{FmrS^J z(iqpJV`jE7adBNZeu8T-xN!P}Bz2#rc=*f(P5TLBnv|&jDFWt(seZ;I-V)Y+)3)TP0{il5zQoXPbaU;NxhSxf2>6( z8ijrnW|=TohuN7DwETccm=+83xvyA>Mqd&{&?`P8A&*<-G z^!GFR`x*WHjQ)N`zo|8{`x*WHjQ)N`e?OzYpV8mX=CZO$vyJ|2qd(i|&o=tAjs9$-KilZfG5T|i{v4w}$LP;7`g4r_9HXDN zz!fjc&N2FPjQ$*>Kga0LG5T|i{v4w}$LP;B`g4u`T%$kN=+8C!Sy4jk%QgCQjs9Gt zKiBBbHTrXn{#>I!*XYkR`g4u`JflC)=+86y`CzivmuK|n8U1-if1c5wXY}V8{dq=z zp3$FY^yeAjsAS2Ki}xjH~RC9{sN=F z!00b9`U{Ny0;9jc=r1t(3yl5(qrbrDFEIKGjQ#?nzrg4(F!~FO{y|3nAftbf(Lc!O zA7u0oGWrJ@{ez7DK}P=|qkoXmKgj4eY$AJ*(Lc!OA7u0oHu?t}{ezAE!AAdJqkpi` zKiKFWZ1fK{`Ue~RgN^>dM*m=w{=r87V55Jq(O+ovn|3L?(C9BT`U{QzLZiRX=r1(- z3yuClqrcGTFEsiKjs8NTztHF}H2RB-{vwn9BBQ^^=r1z*i;Vsvqrb@LFEaXzjQ%2{ zzsTq>GWv^*{vxBl$mlON`iqVJVxzy<=r1<c5PNt(jC)3fNW7_?kOhAU8bc0VW6 z(Vt`5{hUlke@>>+Z`%DF)9&Y(c0b2y_x(jq{0Oyu{6$W?MOf?|q3N-AwiArd6xlml z2k|4Cs8amwCP~$!{vxNm6Po)u<(++PZ$h13LY-bhonAtnUP5heLY-bhZEr$tZ$h13 zLTztConAtnUVo8O-U*FPI+hF=y%FH`$oT0-q|<$o$}7U z(eIRZf00w(3AMiw8vRarXW!^|$~*f;zf<1XH~O9O&c4y_ly~-xey6;%Z}dCm-CyLC zcS57zDevqX`#a^GePe&8yt8lY@054;js2bS&c3m~Q{LG(_IJv=zsM=?gvS0(d1v3G z-zo3xoAf*7oqdyjr@XUo((jaa_D%Yo^3J|Vzf<1XH|clEyT7Q|?;Ytl?i?LB-szNf z&Km7bX=mSPcS<|^j`m`QO=V>|rG!wYicnjXP^XGeTa{4Xmrz@kP~VqOTa{4Xmrz@k zP^XGV!8ytLi7mZW#=5SWn0)V=8yn9!+Bo&hdwkN2@r>m-^7O=OCd_J*lcp1~$xSDY=x7%Ya*RW$jYFu7Q(WX! ziN(cEl}T8bWQM2uPM>u3)cCPurcW3%<%S7k;`Ocjl9;&36DE(-!t@C@I9FXWW#%+3 zCFtmBF>c1}DULwooy%?@eDonV9$T;>GBo#0X@xWoxAc7ls^5U=ky zaq`Sb-eJ!13v>`i7k>J5H8OL)b9kr|oaY4RI>8VpIL8Ujc7kFjC~|^ACm8GmgLIG< zW7=t|3q3AHkL+lNRznot0)1V(mcf|jag3N_&6l@rxlWC3s5``5);#dxC~3Y^vew!3 z1LtJ(wTV|VL1H3P24+mSI<-kw*G?RF&5Wej11ZdwxRxPZ5IEao1=V(u~Qh8R{1oVNl*oOhmy_|7>=WOh#zd75I!gw{- z?;Lf;`kmjzj`W+eEx%#lR6mb#PB;VkT5j8F7~eVVjNWTGEq3hQoNc*lqBCNzkEdR8 zgf}H$(y9<$((<#k*dcu9rq0m4BRj+OT23;9_2xv2wG;F>y*})tf}F&oldC39cP<;% zqRH1Xux`$^IB3eqR^FZ(Gg7awO^q4S*9Vf!n7`g(qK;rrP8?tGw3&e;*iT>{O`#U@jLFRvRnZL*c({O6oHFStU@1(H@m7V`mJebq0-Jb%C+ z@LrZVu-l{F7hh zQTLNSJH^{aP3o9;YNoR9X)8~CTkZQxPOa=CGY9%*A5yO|pzplCKb*D_lx24BGuGLN z19}|`spFZ|&VLmPWh)Jp6YAca*A_-w-&fJ!1n$bFeyx5{o!MJ&O~3xF;nDIt_82j0 z%HN}g(n*ENYlGFw_gl8CIDa^6MwZNq%Sy@0$jZ$+Cus2{DhkacWLn>>M{W z!|llVZF#NcwiPm?P0ePzK+j(r-aK#ZPt1O}Q_oks$GVr7lF z9<$XRWmd!E%vO7nuM$7U?6wz}@le8ihkr6-;#KCWsaX_iM#U0qg|*UJ#cYaCtkuk` z_>_5UYUbK!%v#%IZPByTDy(hHp!(9ZOV8c1m9=J{A^E>_b_tRv=OnORsZbFi9bV5#|6&g`pyH}@*QjH__W zY%4X>%9&@SW?88@R%&ilf}T&+HfGLLpXQlR1L=o4lat@*z&v*_S|oK=)?lzBpG9*{E& zNX`3cnN@R>o>6m~o=Nj}-puWQ_qS(mXD{YB0TdwJ10GI%71$SHNvO6=s zdxDY3qroRuFIfuKSiM|btaMjba2)8teab9fW5h}WT|tIjYUQD&(Ei#gw)gYpp#5rf zcI#YlF}M^Aw*!2MXP-6F4q2CjJNV6=;4W}CxCh(|p5q?RgBQSypai@GUf~}91pflB zg4e*i;63m@SPYhc576-;_y{ZqE6}wHe9XN+;XbPgO9?+ETtm2y->e7aU?bQJz5v_6 zm*6X~6I6j+U=P@9?-xFh2HJrRAj4`edV^E!eWDLI4fsJnkOT7VkSOH3V&rq|QZdB- zT3k&y5ljKIz&)J5pYyD6BOc)TC%{w4PlIQ`vz&Volpw!E_&WL)gAe)b2Et12NgTyK zZ~#R3eU$Jg@Uy*NS|Gs=$+mW>JPx@B^6{V-@=2f%I1djXzvN|L1o9}(UrBfqzq#4| zTHZo9m+%hG-vx-XoCh8SkAwMK|1?+#$R|lY$=3mSBUkX-mHc)cVHx3i!p{iH2{#aK zB-})}nQ#l?R>IE-zaXq2+(x*a@JqrSgkKSoX1SB_Yr-nRYQo*83jhoQ80WF)*vI#Al(6Wh{=zZ)u zt3Ox(o(9i=h2R^zPMi!*1${v#$O75mUAxYe418R7F70?KEqN-l+uJkqye)M|h&m)h z9TK7r2~mH9m{H!2Iv_+H5TXtUQ3r&m145MF5al;S`3+HiLzLeT|-w@?DM446PF~qwxsg%1AWiCWH3sKHOl%WviN0prrWhX@02~l=bISElt zLX?vbWh6uy2~iG0l!FlEAVe8ZJ|Dv8L->3MpAX@SA$&1}FNW~N5WX0~7en}B2wx20 zZz23GgujLGwU8^@N_FLcJkTHHg90!J`<#Jg!dNDZWx`k{jAg=DC5%B7EE2{dVJs5HB4I2N#v)-X62>B7 zEE2{dVJs5HB4I2N#v)-X66Wm(pM95=2D;jJxpKh(a3=Tz$CUqt@xL(s7smg>_+J?R z3*&!b{4b3Eh4H^I{ujpo!uVep{|n=PVf;_sf0X^1YnJ_<>jt~RHOF4;x)IO3$^O7K z*KXh)j3M;)M)1DHDB$MWz2Y`|y?D;9;e42uwYPoHIvHevZ17j`D0mD!4xRu{g85)A zCL2y?jy?WgE9$p0~vt0$P)qa zk*9)7z;N&|cmxn9xfmP<--7SKkKkuu+Xr0&xPS+=0r4Oa98dgCC#3;W8X%tn*z;8VMXnsqBR>sE0N^3`Ah@3NQ{oCy=)VN#4WA<)Z z?i%qi`A8GeS+Ph$wyQmi{sTV7$7b~e3E2$SNt#`QYU9Nu*ybl(GCEx>o z_aXQQEC(z2{VK4I->e7aU?bQJz5v_6m*6X~6I6j+U=P@9R|+ql8wV0VBJkN2u*C}4 zVg+?T~hD{grr;{SvNwiLi=$ z5NA|QcL!)?T6c3H!p;0_Eiibw=&?p`n#Y3ZbVH7Wn;Dr&qFoG9G z@WKdQ7{LoG@xmxx7!eoQJMhFPUKqg(qj+H@o)@Nc`tZC;JZ>Kz7scbEcw8kO7s2C# zcv=K6i{N2VJSd6>RpLQWyd{daMDdg;-cgBXRN@(xct#Y@h~gPhJRyoFMDc`5az9G$ zN6GytIUXgqE6MFja=Vfoj*`PsayUv3N6FnNxf>;SqvURs+>MgEQF1p*?ncSMC^;A< z2czU(l-!Gudl7OkLheP#y$HD%A@?HWUM0C#N$ypWdzG$idxt9rJ-Jp-u9cE&rC7cm%hzN1dMsa$h)NC5f-n<+ND^#PPc+jAkWA03+?4txt>~a2Uf1f%Jo>c6l<1Z%~GsciseeNR4JA! z#ZslDw4RjKlhS%pT2D&rNohSPttX|Wq_mWjmXgv^Qo4wgE+VCiNa-R{x`>o6BBhIX zH?-Ie;=4h7H;C^B@!cT)8pL0N_-hb<4dSms{4$7N2Jy=vei_6sgZO0-zYOA+LHsg^ zUk35ZAbuIdFN64H5WfuKdqI3Ih`$B3pFK_Z4B$F^DTp5h@uMJq6vU5$_)ZYt3F13J zd?$$S1o4|7eiOuRg7{4kUkTzXL3|~MuLSXxAifgBS9sS8>;qv?2e?1J62w=6_(~98 z3DO=dWUTQ<`YK8ERg$nnOCMzj`>LOEG2;!YuQHM=F9%2Kue{4O?}7KhVz30P;M!H- z_x5Fcl)*I64yb-jzLkg#Ro^BFJ34(E)~clSI3S)Ne2VMl6E^jC7IIw)$5g*(AED~| zL^ywB|A*05YV&=v2jTJHX#Js49FOf2&EtC2FM6C1o$_gbCCQKfs-Hy7ypNiBA2stn z`4ecQ?`;7IV*5}D?5lp%u~wq18|V&tf@~{^epM3vswDbVN%X6d=vO7tuS#;wvXWhM zz)e<)>tss=*3q?g~;n5S$6l0=q%I-9R2TkcSQAVFP*CKpr;m_WVfukp9)>gjdk> zSwXnUK1A(&h`Su*E(f{GL2Bniw1J28-6DkQjtTZ5YT-lF!iV(TM-j$qn9u&xU?D&o zweKNn-$T^Chp2tgOIuP#TT;g8%@#&)KBo_INcTZT(kmHF%YQkc>UXRlTm=`*p#RVt zsJ=%&`>OwOkzJ=ojP|UI_N=ojAuuA z_Hy!DSygeW2o{ZC(dAflIo6Dj#}TX+!D`E~+H$P591E?(I#pOlStf#I4q%lC7KvaH z#!&6$SVE1~EvF?`DPJzH#gFGBKMfWFm5OC{HTfSQ))DeQLTn@Svh&E-d|I5-@x_6J zqu~wD(%RAEB~}6QIY=G{$=@LT$sqm72J$yZ3cSi4Zog z#^%-7yb7CFiNPE@hY^XPgjW-eC!E0XiO5sHboOTv-bi>W$L|KO^ZPg1e~;sfkv}9{ z$!Pr=LgK=xMu@zsHf66`R9o%%N>8ddXje-{Mj7#7#Ga5*X7a3>Jgb%&$kEa7;;zTdF!%Wu zd=GvEYNXYs^%lSdJfIDT2Z^AIU4{Qt;XhUQPZc9IAwE~rnUa!6DWQHgwPlo?kCJ=! zcxBZVREIOQgcw(hHKd0!2U+|RgJhEFQ#tB>W6eq zcOLsAz)1QOqrnv%zmk1bqfMnutJ>@-M$;C6XMn1?-oQ$4fp<8+R9Os;(S`m;S8yEY zL7yy+F~$_@zQ(U;owtj}Y`%|SuN5o#dStadT>guZ_OX-@pQ#-J)DtO`iU6g;N2%~p zDtwd*AEm-asqm5h0G{u|^L==}FQ)cTEkpn>_Tj-kY!bkGeR!`A&-G!SfViFSO5IJU zYLxeob*%z__>l7}@y?Gqwg%`LhU1%&w=yoOYM3vvS+&&`5B5>h1o7e!UhKnzeb`a8 z906HirI^|#XlfhPQUv4`9CvD(y?CptZT8}=)HocU3a;lKvp6=JV>cn+0`BGbQ`8{~ zz%$@k&M)HpOB{cLd%gwUL3V1OAl{mSxBBo_AKvQ2TYY$|4{!D1tv^YZC(qu4$L%F2YOr_>R^Efh?ZM+}u(BHetHGi*c-&qr zT7yOR;BkAg=pH<-mi*m~*X6d-1ltq`C%g+bekzO6+TheGRd%A@(&^Ph!8D*sECYCf2)& zrHbKhVpu~AcbgdQB8D}@u!b1!HZiQhD{F}5eqy{QHZh}nLgC+G1WW`zw85}W7!-LrHAoe>*?DiA0{lrAI zvOJ*#BPnI0!IdzFN+%K0=!U&^;5k?3jw4_l+03(dfMHronFgh1ubS}au zTZ9p{2qR(<(p5(*85K3idqDt%KrPr0!k~_8>ba%?`4Bh^z5_o1o3@?%GXfTo@gSM< zPj8>mt_ZDcR5GSQD;r_dE5fK(gw{1mYZ_(5Dl5bOYT%Pmm2u%>j9!Kgb7g2KZBh6;D}jkA?%3^8?)R0Oj@oW%dAN^Z@1X0Czk< zf5=1J&`-S7{SI@#!`$yM_dCq}4s*Z5-0v{ok}4*TV#mfVw%5{UiXL@L>|M^i2i^yZ z!4mL$$HRQowrQXpP-A0i{EIOsJ?8Zk$C}2tN|4o<);>Ztu65*?)^YY)`bJTC0-kx~ zcvkFK);z9L<5`ar{+Hue^o7^bKUz!wXf6Gtwe*kHx-1|;>=;)9`)ZtPE~RS$J=0`z zBAHnlQLJ2tmFpPAspI)pvUM(@GkQ_iJbLjQ*F6tj055_P@Dg~1YyS!U1zrWOf&VnB z@d>{v1#7@Mu3Znx0o>Hu488!{z?a}FuoF~)U0@H`OKEF8!ck{NIO@a@a^NDKhuy<9 ze*u`oM~IQ*3r9vAqh61&V#nCR_xT0azrn0gB18>O8ds_Pk45Ak{RlcD7U?{C!lP_FFyF3E$8}Q$wNwZOFofDeB;j|KKPCT{~kL|=`JMq|# ze~q$N@m#r-QS^7kPHKWWdnp>sq6=lwME`ee}=J{27|p)6p;@wT}o|DXFMDZP-&fUVe+s&}SH^g>$Gf!>B*E|8Jwr7Yv_HD%RZSgJFe2@I2Jx389UD>mJ>-&fL_FPq-h>$dQHA> z-z1m9YnRy%t6rRo_S$s=PXlfOH`^bQ)}@x0SneZ+Apz0eGD?IuEkkoHIkX;cT7@_5AeYv-&R_&_Hs$^X z>b{%6TzfsUh2Trv>9Br+vVgm)rzda=ojY;ptrBynZ*L*@ALHyw_P68vOBw5%jTKm1 znNevEt!Aq023pTKg!E;(_kP|0;Ryu)VG;U@Up|#^W7j(3a~Qj-vf5y&BYQbg&yoEc zsntjJp(|{zsk6LX^DftX!4WmPDa>_hb`v?H^-6O`=gJlAZRg50=E{{^(_qOq7gG0G z2^sCTlM7vW$7P3opS{6m_Mu&3zsOrGzxl5*@S89Fj~Cj^bCat_MeVSH6O%n*P_zSvimj0#KCT`)jB5n&kmAf&o}L<&>m(p7mY9` z7aJ$zS37OK%+J`BWxpJIeT!^AYQJW`#P*i`F73M7tVUa%x7XV%>^BJK+BY}WYs>lU zsQr$;+WEEpp1p-+ukN59O)rfaDGe&=74)&JekRhnAgoji85;iby@z~r3XXJuRSNACWcKUEg$ zbyEypnRN8D|wVZZK0T4X?=-P|Fu5Gedio`r1d^?3Hj6Yzd2z%U9c*idVkGF zneNKlIlc8)+SU7Ot-sLT`rGX4?X_awUpue)3+(C*Hs`zRzxK}B0=~`r3}0Ap{qEW; z&2O*0^NaV_KH=Rp_2$|--uGE=zPi5gKfJ*fjCqIc8!PrLHgC(f)c@_MP|p@`ZBsee&D&+idsoHrvCz$MzIo z8h74Ydn4u>;!E`R!6<-$Ghp<+(*8VroO%He0f{FlXgw3x6*FrOWMEvjkJ~Ki`VLnv@*VPy@79BZ{hpa z6@1%z2j8{+`Zs;k`q$n(OXl0vzx+yd>#tC&?@#j<8a<%3__KO9TDQh{b{cY9Rwc0T z@vg|-c!p)+?Y(&Cxi^~&uRj4^c_N!fuTfy>RSLY6fn?>0!MgEk{qLi;eUd!g9w6Ej-Wo#}={d(lj z*j%i+P>#HT&BYoF8<97$xmkZO?2){aQ$%8sI=rPhz=gnW!R2D!87jNC=A0u}2?9FLqKG8otH zC3+$E7QK;A5GNp?C{9E^Nx)rMS>j~+=BJ2LkWUq-BKHw}ko$_h$ft?ZkTXRlvS0X- zvqTnhKhY04TVx~Wh#cfxkxTzJPvjx@7yTKH%4f8_6YEk8Kt5fZ&Y12%G0^JF8Wof& z)~PrX`46mCkxCtX7IFd4mMv=QA&h&RE5;&^6XO_@xkg-zmB!QVxmkB&0(bihPo6E- zo|r_@6pl<4Q@Q?n!T7M4CZ_SL>8wZL=KbXv$TP)EMha%}wAxZ@Q*dOC zpcSU}zmaQh5^A-xo5jsm0;^T173JoNxmf8|-gEV_a>Z>Nxm`cuzC+xJe3!V3JKW71 zus*eR1xM}^_mQW66@N#5fF}x_Si|BWcG zMN6}Qk)5Z-i^wISgyS!V*Lvb?U;Hp|B3j7 zBdf(~t}hj($e)T&x!W4Cjw5BF3@fb{>pA|J_zWwRi*gn(;LSg*v{7tC*Cw%vvzzsE z{Vie(zuGD&*Sx*`Ir10c3*-t>L2S2)ZOGdhb32yxG`>Qv6qV#VYib}@i7Km`s20`8 zyTmS@6z&$gtz%eagAz?^x)1y87nEq;`VM2a1L6RqRE)fHB*L?qPOQ2S<#@fQ=T{Aa zvdkOd->|Okw}P@Pz7yXee=ol08rI_A2<|T*uu8D@y<9USG_lG9mlF1@yH20 z-RZ>29F$YuBu}x9Wpxf8XPIHck>e!qII|i@cShfOFw?aYt8(;2KAst`omibCpKAul z0ajbq-Z>v3?TNETUftjbZ0e6~Cr`5Z|Z z;Vtzc$mdF09C@BR4|%8@ihRC2-}1@}B&`hZun)If@-lgu)ehz`g6Bkkl(aZ-h>?um zj^gQ4Csy$ojeNPhoa0wWHH+d;@=s{LQjX)uHS!w9+^&^>LB392$DJqY85NV{B(9k( zr*nLUq-3$O$86*qBxOp@k(4Rkl)n-ACT3e$tn_iSmBwlxlqq=1Tx3@LK)zkx&M4>| z@($h-zEe`Bc%%L<=6t0wF!AMvq&s>*cRli%qFp5 zWS=8{!K@MsZdQT3joBp@ENwgTm&`J;;A=aOzmk+7m>VUCRY~YMz~R0|u98*A)v_9S zm)wQ5cgx*ab&uSGTqA3c_sYG<0U1CJ${;fDn=ADOo;kq^iN$UGrL zj>rfy??@om%X%!+AREwlP##1+Bo84UmWRo&Z{#=R(YNwjgfm@MtC=CA3RzgqCuY_7>+vtY8h zutvpY<20A;sJU#s=CYkMmraJr4kUJp%cjI|S(oOrmhKn2;jcru+qrCRIP5TF#a}&| zzj`%)^=SU;*8J6izg`A68o_44WdBH76_-_SyN=?B;vrTjm~SRwxJ?{49gh15#}%VZhtWQTJfAIH^V*J@*Ctqtnt83Onb*EXoLd;JM>AS4 zjCL7!_yAs;s(EdiwUW)!yf#hqTB&)hOY>T(d96$HTB&)hOY_=v&1>D7*QRS;>(;zB zUGrME=C$dX*Sa;YwKT7F>#wy~n#;O1m$fvPb!#qbX)f#5T-MTD)~&g$1(V%LTE1pW zx2o9ORyA8XD`xFN-p!V7?O}7XN>>f?UN$$J_b};FZ1q^{TezlLjn+%MuDGV9xu!>R zO-pl4PYl;g*Id)9xn{cNnqJK{(>2%h!Zq8|)^-pbX#3M)mKMzNSlZOCqASd!o9Kpo z9P5^~5#2?1J8=*L8oaiNb@ms|~+A=A9{;cXrXdGez^x zE}D0yXx`aHt&>LF28+R@v``e1(jrmBkz!Fy9L^SJBcB5|m71Hn)GBHm87hWy{rTd2 zdP*iZAP2&2PuTVlB;L`FaX0HbJeR#*v9|S*h1i zvowp9n#Ee0#inQ$YiSmnuD-&<^@_hr_$#ffX0GWl*SW}wrzXVk)MRl74As&MH35ct z59zwMnWuW-sek3{{o;Po_&4!4&i=ibxu(T1S5FLcO^ac!UbPY%cVI0xe!GyTSr#j@ zJ%YiTZfEL`?I`1JcQT1zw9 zI2i3xWW{9@HJ7#EvL9lBk6^BrX0FLFSK3{8>KcB_`fbRHt){D0+>jMdP1ig%PV>~n z7@nHUw}rNFjpC_o;i)H}z<4>Q$@1 zAuCquj$x%~nw7dWEA0#`ZQz=N;vl?}wc(JzY38OL%}p)1=?@(Lv6-Q|V;HIjhRT=X zSUpZyZCF9hO$CgromhjIg%ukXjzkG8gg6NmYMYJWLM;F zu-J6XV!fKh#%mUvrde#fX0d6o*bJ`eC3~T_x9p94f;@rWo+wX5K1rU$@slO(7wgZR z!tqn(smOe!jN^SJtraWKGmS(-ltY$aK>a13@L4aOG!7%Obgmv?#`GX~yh<{S+g{DK(>2@n!nR%TY!;b=8GBr;U(Gs& zUgS2eHpp?VIOKR3dAergUKn{IauU4Ut$BGz&C63YFYgF1hZnP^A@vXI8uES$Ya6yh zZtrT3+`-iWIo*|x+|kt$xs$6C@-ePskUPWaQ#Geg(40O^bNU3$>C-f)Ptcq`O>_DL z&FRxLr%#8|pT-?BU0GHKRzvKE%vdy>Ua@@M?u6wF&GM6CSbh?#BT`f8F=}B^;e!PD z_T*OI?(7Z&a9McY@Mn;YYeY3d54qn&R!8`PC~Z}MRS$o;cHzUUcK86R9{!Eh5C6(4i1)G@;@y19 z?M~K5yp5F&=kj&8n^`yUM%GWffprvT@#VMQ^PAZ94}ZD(;rmBl|IqDfwQk~X|6Lna zPE>0rs?`(K`iW`<#ciyhsMb*2!HSIUv67-%OHr++sMb?dD=Mlr6{}fQaTlv9s&y6B z%8Gkk*Rcil`ijm9i)xKUwaTJeXYl|lEvmH^)oP1T)>~97E~+&b)vAk!Sa(sayr|Y* zRI4wl^%vC&jA{)=wF;wJhf%G>*vQ(Di(R%}k&#B9jRjp<(J{bkj0vpC81If_YvcB^ zdD!@4{YXaR7!7ZypL1l;J4>(%=`CgGp3#|%`ajA30{T6d(&u@J-qkCN)c=_t%?f%@ ztLTf&r1!DQx{aPdkY3Y1tASs9!x;5HtRIE6US!-k!Fr7`<#yIPu=lg9rRrOcT;1YN z%dN`a zaB^>EX>4U6ba`-PAZ2)IW&i+q+Rd0*wi~?-gx|S}EP*6Qf?N*AbFzahKOflA9c=%D zlRU)oRkylZ98iTqL2m!+pU3@!f0CMaDQhjQ6wklZQj49F>Ysn@z6YP~`~4SRxA^?+ zy8E~hxg;Fr`KbN%eCK-p{K9*VFuq@R^>ruK*Fdiu?*YYTw%OL#MRGkp*F$;@)b?{y zTGg*h9j~R%*MoN{uRqaehwnk`v-^4>DN$OPrzFNLK|JqKUMrC2uiN$Ko#%p7&aW0s0o?_nG@K`X6UM1V8rIb@?Hd zMj3wow4K-AIjf$tpWXFTh-i5+%DZ=T>^nR-2)W#^6<*3;=3MWq@v6T22{?=C2Co@f zh{XN4kirT*oUre4hRG6B+)+8kIHTNaRW!CZ#c)dY3MZ;GS)dkW#_dG796#0)?!4{J zH$!FS6?ilT1`B-U4`1%jZvM{8onx^L!Pk#iv94&$Wf_K?z4H`+g!95J-vGZ~FZlfz z{y(p4@LcBjWJPegUIP#!=FTi82nhHRB!?2b#;73>#|l85 zl}AZ&fI!GOE*2S+NQq8kjW##m(Qu!QIhJUb(I$Y%QbfU~Ne8TyGW^k0;ZV<#Q_i_$ z&DG_Wdy$e#E~RKOk?Sg&G;2}Ss!e;9nrg14YOS@^-eMCpFt^gQ)z(_?(RC*3jMaI* zb7jOKBaJ-D&{0PleL|m^W}ap0th3F&;*y0-tg>{~)mGoZZIoiCop;%~>u$RrxOU{E zlTSH%>S?Ecp!P!br`K? za)z02k)+6xMP|dstzd`&VcH?)6F#u}MDB0l=5qa8xW&Ih&KY$73*;Qo{fyf$P+L~# z=0)tuLW`*vvhRe_V|6`_-72cb2mb%ZNB7?j@c%QAEJ8E0ww86plii7<^?7!e*-qj7 z>k$$yR@`rE|qyo?Wbo4Xk|bFCX!@0%NcEsIm#Zo_StC+Bn;ZYt+Er2o2lHvN7##dyc~NjXRw_;qV6TLTN0?vhJtQ}JVk2QF`iCHSRh{*n#Z<* zqSQ_Tk`}IO%ZoMS8?v&eO3WIET_bICTe*)tnHXKEhplv*Bd6Vm)IalJ8 z!Uv*8gbH|0TlMLU+#bloV$Y+vp-i>QO1r5rxom(teROEcs&oc8(@@)B=JR zY5N|C8~Y6h(9%)E?Z=Fpp)8N`AhfY^WtV^ve=fr|V zB<^CEkmSch&^^#|_SGzNpcd=_-=icxj6P&ej$K_b4WO!6m|KpjaW*DNC`KnGHeo%> zYfVVU$qH|}(dJG*lmbU_$a@C&O1|1&$@d4j_p2jnG;X4^R&^c8+9|Y#)hU_PFrTu9 zQ=*3ll|ofnIq|7GlDUge=Sm%do67oSq-1so00nrVv1}I{ zEFFAyAxGJ>tW`F4iW@+XL5TFy;D_0ifGwbsw46~Pj)0tuUL)CnsCoxJr-ewWK6Ki> zXp6tYClb%YD5fW$#+07~W0(MirNZU?r;@I`6%PYC^*9r@zF(Q*(J61MkWDQoU{iEAf zg_pR2Hc%Bz%S=_#18H((l6M!Sa{S8YGq$GXdAtdoevIuGGfE=_Ph=I8kI}a@ z;~`p$5)-*>p-r+{HOaL)MD69PYB5{IZGUL**=M(z16-9k?8kta+P)^6d`p@7VcvaL zGMDs2ml!3{Cr+qGe3R>^(w=R+1}FXM68+#-a?A7*9${kEGE0J#J;%|%Yz$#qs#dbu z)bx+T3W$&O($$EIx{^7a=|Ua?h+AJLL!4xL-&x#dt^i!a%Qnjr)E`V5x)@okP4a5nm90e8pqhKzWQB#Lm_2a#S9Y^*i=x&zknIcGWlO47)SfQo z;EM2ORuBeZ#jl&dVkKLf^+00By3KlB9_8@q{~qQI6Li9uZh6Iy74j98wvLc$$g2=b z{6%w=sCe zFzE#x`KWE*esQd-lO{Vn4O3;R`R?UY_+>#h?}gVU8m{Hmc0f2;>-dl_1P(J!Pmff# ziL)dzN*OfuV!aVl&>I;IUhP*vvsq{+5Ror#duJ6a_u#E?H6Bla+GG@0&Gwmomq1H_ zI%6Q#wHncK!<{&dK^C`m`#DqeLVv}UR~T2I;IDDr3a|`AqJ{UgywjPFu^m+gzbAC- z=SxTU`#;+J+(AD0(lQVf>}4_8?Ez)BU5Y&0izZc;M4-Sfiq$o;9|$0s{aP082Q3Yt zKF)7>>yQ3ebkqa@t+Aq`P0cA|Mwy3{rY^pIJt@WiZoBofW6OxfT!>bzM?7jBa6$H) z2W^Kf%k1v$E9n~}$-B<1)1u0LT^CC`+}xo%n1MbGr&dg(t|%P)CBj-pL5eZedZNU5 z!^muOvt~h2Zu?2Y%I5&lX8tczXUWfRbB8-Mj49=5iYg8}%{=JLnFz%G4d+@b#4@lO z7ytkPg=s@WP)S2WAaHVTW@&6?001bFeUUv#!$25@-?mDnR2(cK;*g;_Sr99tn^vI+ z6+*4hs)Na;f6$~MNpW!$Tni5VELI&{oON|@6$HT_5GO|`MHeaYyQI(}#ygICc<*~( z?!E*3jS5rEu5m!sEF+yv2-)1K5PU_5<^bZDk(jB^iDC+# zFe@1<@g#9nQ8miE{;g^C?+4(ga?=xIgTVj*00v@9M??UB0RI4g z{Nt1%00009a7bBm000XU000XU0RWnu7ytkO2XskIMF->x3Klst&GR^)000GDNklTWl0%6vuxvd+l~hx3CmVfh{X7+m6&|CAJZAYHz%T%dN+sTyA0ovNe{&)_{}yMa5+A_M-*hZ^+hleO}_g_V$FFyVmrw;w}HMbs<_8_?nON%f1|zf0!24XycIG?68dup3$ zY)ogtiqVPHK#BpDuXv8KdFMELq=kw_!67Lx$&+cSImd{U0ZgXA;QxDV?D(dWOAT(i zI+O3~JZ1&}4s377;qal=>bY}A=HMY4PG=9}#`YkH3LpSl!ji(oVkZFAD{qrOxu4ve zPK?G7Mq`jMdDEh1CnkWBJeeKaPJ^HUvJiz9@kwrj*K!l^m zY%HxB^>A%i*DT`ojsYS-4zlu+8nviCS%3otNHPTp1of(O8<2}$FY*1hv`6ECQ)fmX z3JP!tkbpVBMBr&44Nw3*paYVCS%4ili`V-kHK${{(8dEW#o33f2tWX60p_WZgD74H zCL_yE;8_*WL7*8?P^fRP#BI-*02a)anBu%jNYbg1O2BoX5J*O=9|c4R1s1B&*Q!U9 zghq3RdUw*JQSnFbpC=?afC<1^lpMdCkW|kgqdopr9tQayfqyD{Sxce{XuJM>pd3T zcaL=8B6c0rfM|#fpmI$rAXAbzg7u#sWBhnOvN95wi!9p-1+*AU0-~q@GIB&qC;)5M z{=}+Pt0*ce8e;Fni4#;*RA5RnGBvZAQx_+KCMkXq)-{XFn3u`Llcy0xEpSflgcs@W z9*y338wd~zqyW8KxNw0aM?Svk}p=*Rgt-w{_cw9T3ot>Yy8eo$rf_i}CyYD9YW7 zq^72Zva+)HJW!k?Gd813t@L)F9gP@4r|m*O0>%NYKp)_XYG?1>z4-lpEEWqEi)An) zaRF@j>_GKTNA<}hb|0X|0K=SshL9(+tkV0C%-I7Q4^n7M7&R~rz8TVfzo4y{8Ue^mm&}qZ`SvPKQe_LA{ z<>lqn*4E02O~Z6{9n{fw{t2|2 z2s?J>u(;9+fFwy&S65@R*_btJ7Ik%XELpOI($Z3FHXGH|)ku;Q8^DIoFH>1nFgOqP zN10FE?lcy?ZxXwrruWu#o-x_hYqMak*URbh_wF_(|0* zN?x9j(4@QbXER_Pww{`r%EpZwX=-W$AS){ihr_|#xpRj&zb}B*Uzro0^G~)$uVKAj zkKJx(&z?OP42FjvNFGG&b~|3L7mH=+AW~mnKjho%^^%>PO>1|Yq4a@e;1ao+*YJ2e oG1YQ&bMaixfF#?04B#Q;Z+5ojoR~&;z5oCK07*qoM6N<$f);9=`2YX_ literal 0 HcmV?d00001 diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 0eade6292f9..247739060b0 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -24,6 +24,7 @@ #include "editor/button_widget.hpp" #include "editor/layer_icon.hpp" #include "editor/object_info.hpp" +#include "editor/particle_editor.hpp" #include "editor/resize_marker.hpp" #include "editor/tile_selection.hpp" #include "editor/tip.hpp" @@ -32,7 +33,6 @@ #include "gui/dialog.hpp" #include "gui/menu_manager.hpp" #include "gui/mousecursor.hpp" -#include "gui/mousecursor.hpp" #include "math/util.hpp" #include "object/camera.hpp" #include "object/player.hpp" @@ -85,6 +85,7 @@ Editor::Editor() : m_deactivate_request(false), m_save_request(false), m_test_request(false), + m_particle_editor_request(false), m_test_pos(), m_savegame(), m_sector(), @@ -182,6 +183,13 @@ Editor::update(float dt_sec, const Controller& controller) return; } + if (m_particle_editor_request) { + m_particle_editor_request = false; + std::unique_ptr screen(new ParticleEditor()); + ScreenManager::current()->push_screen(move(screen)); + return; + } + if (m_deactivate_request) { m_enabled = false; m_deactivate_request = false; diff --git a/src/editor/editor.hpp b/src/editor/editor.hpp index 9433694912b..127c32b3ff7 100644 --- a/src/editor/editor.hpp +++ b/src/editor/editor.hpp @@ -155,6 +155,7 @@ class Editor final : public Screen, bool m_deactivate_request; bool m_save_request; bool m_test_request; + bool m_particle_editor_request; boost::optional> m_test_pos; std::unique_ptr m_savegame; diff --git a/src/editor/object_menu.cpp b/src/editor/object_menu.cpp index 041aca2b015..c5ad5852a74 100644 --- a/src/editor/object_menu.cpp +++ b/src/editor/object_menu.cpp @@ -68,6 +68,11 @@ ObjectMenu::menu_action(MenuItem& item) break; } + case MNID_OPEN_PARTICLE_EDITOR: + m_editor.m_particle_editor_request = true; + MenuManager::instance().pop_menu(); + break; + default: break; } diff --git a/src/editor/object_menu.hpp b/src/editor/object_menu.hpp index 2a8d73ecbe2..3e2f5b0ea70 100644 --- a/src/editor/object_menu.hpp +++ b/src/editor/object_menu.hpp @@ -27,7 +27,8 @@ class ObjectMenu final : public Menu public: enum { MNID_REMOVE, - MNID_TEST_FROM_HERE + MNID_TEST_FROM_HERE, + MNID_OPEN_PARTICLE_EDITOR, }; public: diff --git a/src/editor/object_option.cpp b/src/editor/object_option.cpp index b64d8b0650c..047fb52a01b 100644 --- a/src/editor/object_option.cpp +++ b/src/editor/object_option.cpp @@ -583,4 +583,39 @@ TestFromHereOption::add_to_menu(Menu& menu) const menu.add_entry(ObjectMenu::MNID_TEST_FROM_HERE, get_text()); } +ParticleEditorOption::ParticleEditorOption() : + ObjectOption(_("Open Particle Editor"), "", 0) +{ +} + +std::string +ParticleEditorOption::to_string() const +{ + return {}; +} + +void +ParticleEditorOption::add_to_menu(Menu& menu) const +{ + menu.add_entry(ObjectMenu::MNID_OPEN_PARTICLE_EDITOR, get_text()); +} + +ButtonOption::ButtonOption(const std::string& text, const std::function callback) : + ObjectOption(text, "", 0), + m_callback(callback) +{ +} + +std::string +ButtonOption::to_string() const +{ + return {}; +} + +void +ButtonOption::add_to_menu(Menu& menu) const +{ + menu.add_entry(get_text(), m_callback); +} + /* EOF */ diff --git a/src/editor/object_option.hpp b/src/editor/object_option.hpp index 9d4a3a0ef50..233592d400c 100644 --- a/src/editor/object_option.hpp +++ b/src/editor/object_option.hpp @@ -395,6 +395,37 @@ class TestFromHereOption : public ObjectOption TestFromHereOption& operator=(const TestFromHereOption&) = delete; }; +class ParticleEditorOption : public ObjectOption +{ +public: + ParticleEditorOption(); + + virtual void save(Writer& write) const override {} + virtual std::string to_string() const override; + virtual void add_to_menu(Menu& menu) const override; + +private: + ParticleEditorOption(const ParticleEditorOption&) = delete; + ParticleEditorOption& operator=(const ParticleEditorOption&) = delete; +}; + +class ButtonOption : public ObjectOption +{ +public: + ButtonOption(const std::string& text, const std::function callback); + + virtual void save(Writer& write) 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; +}; + #endif /* EOF */ diff --git a/src/editor/object_settings.cpp b/src/editor/object_settings.cpp index 7abfa99be7d..6ec3525fd1b 100644 --- a/src/editor/object_settings.cpp +++ b/src/editor/object_settings.cpp @@ -291,6 +291,18 @@ ObjectSettings::add_test_from_here() add_option(std::make_unique()); } +void +ObjectSettings::add_particle_editor() +{ + add_option(std::make_unique()); +} + +void +ObjectSettings::add_button(const std::string& text, const std::function callback) +{ + add_option(std::make_unique(text, callback)); +} + void ObjectSettings::reorder(const std::vector& order) { diff --git a/src/editor/object_settings.hpp b/src/editor/object_settings.hpp index b4c570e8261..418c1fa427b 100644 --- a/src/editor/object_settings.hpp +++ b/src/editor/object_settings.hpp @@ -136,6 +136,10 @@ class ObjectSettings final void add_sexp(const std::string& text, const std::string& key, sexp::Value& value, unsigned int flags = 0); void add_test_from_here(); + void add_particle_editor(); + + // VERY UNSTABLE - use with care ~ Semphris (author of that option) + void add_button(const std::string& text, const std::function callback); const std::vector >& get_options() const { return m_options; } diff --git a/src/editor/particle_editor.cpp b/src/editor/particle_editor.cpp new file mode 100644 index 00000000000..3dec6405113 --- /dev/null +++ b/src/editor/particle_editor.cpp @@ -0,0 +1,701 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "editor/particle_editor.hpp" + +#include "control/input_manager.hpp" +#include "editor/editor.hpp" +#include "gui/dialog.hpp" +#include "gui/menu_manager.hpp" +#include "gui/menu_filesystem.hpp" +#include "gui/mousecursor.hpp" +#include "math/easing.hpp" +#include "object/custom_particle_system.hpp" +#include "physfs/util.hpp" +#include "supertux/menu/menu_storage.hpp" +#include "supertux/menu/particle_editor_save_as.hpp" +#include "supertux/screen_fade.hpp" +#include "supertux/screen_manager.hpp" +#include "util/file_system.hpp" +#include "util/reader.hpp" +#include "util/reader_document.hpp" +#include "util/reader_mapping.hpp" +#include "video/compositor.hpp" + +#include +#include + +bool +ParticleEditor::is_active() +{ + auto* self = ParticleEditor::current(); + return self && true; +} + +ParticleEditor::ParticleEditor() : + m_enabled(true), + m_quit_request(false), + m_controls(), + m_undo_stack(), + m_redo_stack(), + m_saved_version(), + m_particles(), + m_filename("") +{ + reload(); +} + +void +ParticleEditor::reload() +{ + // TODO: Use a std::unique_ptr here + if (m_particles) + delete m_particles; + + auto doc = ReaderDocument::from_file((m_filename == "") ? "/particles/default.stcp" : m_filename); + auto root = doc.get_root(); + auto mapping = root.get_mapping(); + + if (root.get_name() != "supertux-custom-particle") + throw std::runtime_error("file is not a supertux-custom-particle file."); + + m_particles = new CustomParticleSystem(mapping); + + m_controls.clear(); + + // TODO: Use the addButton() command + // Texture button start + auto texture_btn = std::make_unique("Change texture..."); + texture_btn.get()->m_on_change = new std::function([this](){ + const std::vector& filter = {".jpg", ".png", ".surface"}; + MenuManager::instance().push_menu(std::make_unique( + &(m_particles->m_particle_main_texture), + filter, + "/", + [this](std::string new_filename) { m_particles->reinit_textures(); } + )); + }); + float tmp_height = 0.f; + for (auto& control : m_controls) { + tmp_height = std::max(tmp_height, control->get_rect().get_bottom() + 5.f); + } + texture_btn.get()->set_rect(Rectf(25.f, tmp_height, 325.f, tmp_height + 20.f)); + m_controls.push_back(std::move(texture_btn)); + // Texture button end + + addTextboxInt(_("Max amount"), &(m_particles->m_max_amount), [](ControlTextboxInt* ctrl, int i) + { + if (i < 0) { + ctrl->set_value(0); + return false; + } else if (i > 500) { + ctrl->set_value(500); + return false; + } else { + return true; + } + } + ); + addTextboxFloat(_("Delay"), &(m_particles->m_delay)); + addCheckbox(_("Spawn anywhere"), &(m_particles->m_cover_screen)); + addTextboxFloatWithImprecision(_("Life duration"), + &(m_particles->m_particle_lifetime), + &(m_particles->m_particle_lifetime_variation), + [](ControlTextboxFloat* ctrl, float f){ return f > 0.f; }, + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }); + addTextboxFloatWithImprecision(_("Birth duration"), + &(m_particles->m_particle_birth_time), + &(m_particles->m_particle_birth_time_variation), + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }, + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }); + addTextboxFloatWithImprecision(_("Death duration"), + &(m_particles->m_particle_death_time), + &(m_particles->m_particle_death_time_variation), + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }, + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }); + + auto birth_mode = std::make_unique >(); + birth_mode.get()->add_option(CustomParticleSystem::FadeMode::Shrink, _("Grow")); + birth_mode.get()->add_option(CustomParticleSystem::FadeMode::Fade, _("Fade in")); + birth_mode.get()->add_option(CustomParticleSystem::FadeMode::None, _("None")); + birth_mode.get()->bind_value(&(m_particles->m_particle_birth_mode)); + addControl(_("Birth mode"), std::move(birth_mode)); + auto death_mode = std::make_unique >(); + death_mode.get()->add_option(CustomParticleSystem::FadeMode::Shrink, _("Shrink")); + death_mode.get()->add_option(CustomParticleSystem::FadeMode::Fade, _("Fade out")); + death_mode.get()->add_option(CustomParticleSystem::FadeMode::None, _("None")); + death_mode.get()->bind_value(&(m_particles->m_particle_death_mode)); + addControl(_("Death mode"), std::move(death_mode)); + + addEasingEnum(_("Birth easing"), &(m_particles->m_particle_birth_easing)); + addEasingEnum(_("Death easing"), &(m_particles->m_particle_death_easing)); + + addTextboxFloatWithImprecision(_("Horizontal speed"), + &(m_particles->m_particle_speed_x), + &(m_particles->m_particle_speed_variation_x), + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }, + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }); + addTextboxFloatWithImprecision(_("Vertical speed"), + &(m_particles->m_particle_speed_y), + &(m_particles->m_particle_speed_variation_y), + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }, + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }); + addTextboxFloat(_("Horizontal acceleration"), &(m_particles->m_particle_acceleration_x)); + addTextboxFloat(_("Vertical acceleration"), &(m_particles->m_particle_acceleration_y)); + addTextboxFloat(_("Horizontal friction"), &(m_particles->m_particle_friction_x)); + addTextboxFloat(_("Vertical friction"), &(m_particles->m_particle_friction_y)); + addTextboxFloat(_("Feather factor"), &(m_particles->m_particle_feather_factor)); + addTextboxFloatWithImprecision(_("Initial rotation"), + &(m_particles->m_particle_rotation), + &(m_particles->m_particle_rotation_variation), + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }, + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }); + addTextboxFloatWithImprecision(_("Rotation speed"), + &(m_particles->m_particle_rotation_speed), + &(m_particles->m_particle_rotation_speed_variation), + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }, + [](ControlTextboxFloat* ctrl, float f){ return f >= 0.f; }); + addTextboxFloat(_("Rotation acceleration"), &(m_particles->m_particle_rotation_acceleration)); + addTextboxFloat(_("Rotation friction/decceleration"), &(m_particles->m_particle_rotation_decceleration)); + + auto rotation_mode = std::make_unique >(); + rotation_mode.get()->add_option(CustomParticleSystem::RotationMode::Wiggling, _("Wiggling")); + rotation_mode.get()->add_option(CustomParticleSystem::RotationMode::Facing, _("Facing")); + rotation_mode.get()->add_option(CustomParticleSystem::RotationMode::Fixed, _("Fixed")); + rotation_mode.get()->bind_value(&(m_particles->m_particle_rotation_mode)); + addControl(_("Rotation mode"), std::move(rotation_mode)); + + auto collision_mode = std::make_unique >(); + collision_mode.get()->add_option(CustomParticleSystem::CollisionMode::Destroy, _("Destroy")); + collision_mode.get()->add_option(CustomParticleSystem::CollisionMode::BounceLight, _("Bounce (light)")); + collision_mode.get()->add_option(CustomParticleSystem::CollisionMode::BounceHeavy, _("Bounce (heavy)")); + collision_mode.get()->add_option(CustomParticleSystem::CollisionMode::Stick, _("Stick to surface")); + collision_mode.get()->add_option(CustomParticleSystem::CollisionMode::Ignore, _("No collision")); + collision_mode.get()->bind_value(&(m_particles->m_particle_collision_mode)); + addControl(_("Collision mode"), std::move(collision_mode)); + + auto offscreen_mode = std::make_unique >(); + offscreen_mode.get()->add_option(CustomParticleSystem::OffscreenMode::Always, _("Always destroy")); + offscreen_mode.get()->add_option(CustomParticleSystem::OffscreenMode::OnlyOnExit, _("Only on exit")); + offscreen_mode.get()->add_option(CustomParticleSystem::OffscreenMode::Never, _("Never")); + offscreen_mode.get()->bind_value(&(m_particles->m_particle_offscreen_mode)); + addControl(_("Offscreen mode"), std::move(offscreen_mode)); + + // FIXME: 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 = new std::function([this](){ m_particles->clear(); }); + float height = 0.f; + for (auto& control : m_controls) { + height = std::max(height, control->get_rect().get_bottom() + 5.f); + } + clear_btn.get()->set_rect(Rectf(25.f, height, 325.f, height + 20.f)); + m_controls.push_back(std::move(clear_btn)); + + m_undo_stack.clear(); + m_saved_version = m_particles->get_props(); + m_undo_stack.push_back(m_saved_version); +} + +ParticleEditor::~ParticleEditor() +{ +} + +void +ParticleEditor::addTextboxFloat(std::string name, float* bind, bool (*float_validator)(ControlTextboxFloat*, float)) +{ + auto float_control = std::make_unique(); + float_control.get()->bind_value(bind); + float_control.get()->m_validate_float = float_validator; + addControl(name, std::move(float_control)); +} + +void +ParticleEditor::addTextboxFloatWithImprecision(std::string name, float* bind, + float* imprecision_bind, + bool (*float_validator)(ControlTextboxFloat*, float), + bool (*imp_validator)(ControlTextboxFloat*, float)) +{ + auto float_control = std::make_unique(); + float_control.get()->bind_value(bind); + float_control.get()->m_validate_float = float_validator; + + auto imp_control = std::make_unique(); + imp_control.get()->bind_value(imprecision_bind); + imp_control.get()->m_validate_float = imp_validator; + + // Can't use addControl() because this is a special case + float height = 0.f; + for (auto& control : m_controls) { + height = std::max(height, control->get_rect().get_bottom() + 5.f); + } + + float_control.get()->set_rect(Rectf(150.f, height, 235.f, height + 20.f)); + imp_control.get()->set_rect(Rectf(265.f, height, 350.f, height + 20.f)); + + float_control.get()->m_label = new InterfaceLabel(Rectf(5.f, height, 145.f, height + 20.f), name); + imp_control.get()->m_label = new InterfaceLabel(Rectf(240.f, height, 260.f, height + 20.f), "±"); + + float_control.get()->m_on_change = new std::function([this](){ this->push_version(); }); + imp_control.get()->m_on_change = new std::function([this](){ this->push_version(); }); + + m_controls.push_back(std::move(float_control)); + m_controls.push_back(std::move(imp_control)); +} + +void +ParticleEditor::addTextboxInt(std::string name, int* bind, bool (*int_validator)(ControlTextboxInt*, int)) +{ + auto int_control = std::make_unique(); + int_control.get()->bind_value(bind); + int_control.get()->m_validate_int = int_validator; + addControl(name, std::move(int_control)); +} + +void +ParticleEditor::addCheckbox(std::string name, bool* bind) +{ + auto bool_control = std::make_unique(); + bool_control.get()->bind_value(bind); + bool_control.get()->set_rect(Rectf(240.f, 0.f, 260.f, 20.f)); + addControl(name, std::move(bool_control)); +} + +void +ParticleEditor::addEasingEnum(std::string name, EasingMode* bind) +{ + // FIXME: They don't render in the same order as in here + auto ease_ctrl = std::make_unique >(); + ease_ctrl.get()->add_option(EaseNone, _(getEasingName(EaseNone))); + ease_ctrl.get()->add_option(EaseQuadIn, _(getEasingName(EaseQuadIn))); + ease_ctrl.get()->add_option(EaseQuadOut, _(getEasingName(EaseQuadOut))); + ease_ctrl.get()->add_option(EaseQuadInOut, _(getEasingName(EaseQuadInOut))); + ease_ctrl.get()->add_option(EaseCubicIn, _(getEasingName(EaseCubicIn))); + ease_ctrl.get()->add_option(EaseCubicOut, _(getEasingName(EaseCubicOut))); + ease_ctrl.get()->add_option(EaseCubicInOut, _(getEasingName(EaseCubicInOut))); + ease_ctrl.get()->add_option(EaseQuartIn, _(getEasingName(EaseQuartIn))); + ease_ctrl.get()->add_option(EaseQuartOut, _(getEasingName(EaseQuartOut))); + ease_ctrl.get()->add_option(EaseQuartInOut, _(getEasingName(EaseQuartInOut))); + ease_ctrl.get()->add_option(EaseQuintIn, _(getEasingName(EaseQuintIn))); + ease_ctrl.get()->add_option(EaseQuintOut, _(getEasingName(EaseQuintOut))); + ease_ctrl.get()->add_option(EaseQuintInOut, _(getEasingName(EaseQuintInOut))); + ease_ctrl.get()->add_option(EaseSineIn, _(getEasingName(EaseSineIn))); + ease_ctrl.get()->add_option(EaseSineOut, _(getEasingName(EaseSineOut))); + ease_ctrl.get()->add_option(EaseSineInOut, _(getEasingName(EaseSineInOut))); + ease_ctrl.get()->add_option(EaseCircularIn, _(getEasingName(EaseCircularIn))); + ease_ctrl.get()->add_option(EaseCircularOut, _(getEasingName(EaseCircularOut))); + ease_ctrl.get()->add_option(EaseCircularInOut, _(getEasingName(EaseCircularInOut))); + ease_ctrl.get()->add_option(EaseExponentialIn, _(getEasingName(EaseExponentialIn))); + ease_ctrl.get()->add_option(EaseExponentialOut, _(getEasingName(EaseExponentialOut))); + ease_ctrl.get()->add_option(EaseExponentialInOut, _(getEasingName(EaseExponentialInOut))); + ease_ctrl.get()->add_option(EaseElasticIn, _(getEasingName(EaseElasticIn))); + ease_ctrl.get()->add_option(EaseElasticOut, _(getEasingName(EaseElasticOut))); + ease_ctrl.get()->add_option(EaseElasticInOut, _(getEasingName(EaseElasticInOut))); + ease_ctrl.get()->add_option(EaseBackIn, _(getEasingName(EaseBackIn))); + ease_ctrl.get()->add_option(EaseBackOut, _(getEasingName(EaseBackOut))); + ease_ctrl.get()->add_option(EaseBackInOut, _(getEasingName(EaseBackInOut))); + ease_ctrl.get()->add_option(EaseBounceIn, _(getEasingName(EaseBounceIn))); + ease_ctrl.get()->add_option(EaseBounceOut, _(getEasingName(EaseBounceOut))); + ease_ctrl.get()->add_option(EaseBounceInOut, _(getEasingName(EaseBounceInOut))); + ease_ctrl.get()->bind_value(bind); + addControl(name, std::move(ease_ctrl)); +} + +void +ParticleEditor::addControl(std::string name, std::unique_ptr new_control) +{ + float height = 0.f; + for (auto& control : m_controls) { + height = std::max(height, control->get_rect().get_bottom() + 5.f); + } + + if (new_control.get()->get_rect().get_width() == 0.f || new_control.get()->get_rect().get_height() == 0.f) { + new_control.get()->set_rect(Rectf(150.f, height, 350.f, height + 20.f)); + } else { + new_control.get()->set_rect(Rectf(new_control.get()->get_rect().get_left(), + height, + new_control.get()->get_rect().get_right(), + height + new_control.get()->get_rect().get_height())); + } + + new_control.get()->m_label = new InterfaceLabel(Rectf(5.f, height, 135.f, height + 20.f), name); + new_control.get()->m_on_change = new std::function([this](){ this->push_version(); }); + m_controls.push_back(std::move(new_control)); +} + + +void +ParticleEditor::save(const std::string& filepath_, bool retry) +{ + std::string filepath = filepath_; + if (!boost::algorithm::ends_with(filepath, ".stcp")) + filepath += ".stcp"; + + //FIXME: It tests for directory in supertux/data, but saves into .supertux2. + try { + { // make sure the level directory exists + std::string dirname = FileSystem::dirname(filepath); + if (!PHYSFS_exists(dirname.c_str())) + { + if (!PHYSFS_mkdir(dirname.c_str())) + { + std::ostringstream msg; + msg << "Couldn't create directory for particle config '" + << dirname << "': " <save(writer); + + // Ends writing to supertux particle file. Keep this at the very end. + writer.end_list("supertux-custom-particle"); +} + +void +ParticleEditor::request_save(bool is_save_as, std::function callback) +{ + if (is_save_as || m_filename == "") + { + m_enabled = false; + MenuManager::instance().set_menu(std::make_unique(callback)); + } + else + { + save(m_filename); + callback(true); + } +} + +void +ParticleEditor::draw(Compositor& compositor) +{ + auto& context = compositor.make_context(); + + m_particles->draw(context); + + /*context.color().draw_filled_rect(Rectf(0.f, 0.f, 255.f, context.get_height()), + Color(), + LAYER_GUI - 1);*/ + context.color().draw_gradient(Color(0.05f, 0.1f, 0.1f, 1.f), + Color(0.1f, 0.15f, 0.15f, 1.f), + LAYER_GUI - 1, + GradientDirection::HORIZONTAL, + Rectf(0.f, 0.f, 355.f, float(context.get_height()))); + + for(const auto& control : m_controls) { + control->draw(context); + } + + MouseCursor::current()->draw(context); +} + +void +ParticleEditor::update(float dt_sec, const Controller& controller) +{ + + update_keyboard(controller); + + if (m_quit_request) { + quit_editor(); + } + + for(const auto& control : m_controls) { + control->update(dt_sec, controller); + } + + // Uncomment to make the particles stop updating on pause + //if (!m_enabled) + // return; + + m_particles->update(dt_sec); +} + +void +ParticleEditor::update_keyboard(const Controller& controller) +{ + if (!m_enabled) + return; + + if (controller.pressed(Control::ESCAPE)) { + m_enabled = false; + MenuManager::instance().set_menu(MenuStorage::PARTICLE_EDITOR_MENU); + return; + } +} + +void +ParticleEditor::quit_editor() +{ + m_quit_request = false; + + auto quit = [] () + { + //Quit particle editor + /*m_world = nullptr; + m_levelfile = ""; + m_levelloaded = false; + m_enabled = false; + Tile::draw_editor_images = false;*/ + ScreenManager::current()->pop_screen(); + if (Editor::current()) { + Editor::current()->m_reactivate_request = true; + } + }; + + check_unsaved_changes([quit] { + quit(); + }); +} + +void +ParticleEditor::leave() +{ + m_enabled = false; +} + +void +ParticleEditor::setup() +{ + m_enabled = true; +} + +void +ParticleEditor::check_unsaved_changes(const std::function& action) +{ + if (m_undo_stack.back() != m_saved_version) + { + m_enabled = false; + auto dialog = std::make_unique(); + dialog->set_text(_("This particle configuration contains unsaved changes, do you want to save?")); + dialog->add_button(_("Save"), [this, action] { + request_save(false, [this, action] (bool was_saved) { + m_enabled = true; + if (was_saved) + action(); + }); + }); + dialog->add_default_button(_("Save as"), [this, action] { + request_save(true, [this, action] (bool was_saved) { + m_enabled = true; + if (was_saved) + action(); + }); + }); + dialog->add_button(_("No"), [this, action] { + action(); + m_enabled = true; + }); + dialog->add_button(_("Cancel"), [this] { + m_enabled = true; + }); + MenuManager::instance().set_dialog(std::move(dialog)); + } + else + { + action(); + } +} + +void +ParticleEditor::reactivate() +{ + m_enabled = true; +} + +void +ParticleEditor::open_particle_directory() +{ + auto path = FileSystem::join(PHYSFS_getWriteDir(), "/particles/custom/"); + FileSystem::open_path(path); +} + +void +ParticleEditor::event(const SDL_Event& ev) +{ + if (!m_enabled) return; + + for(const auto& control : m_controls) { + if (control->event(ev)) + break; + } + + if (ev.type == SDL_KEYDOWN && + ev.key.keysym.sym == SDLK_z && + ev.key.keysym.mod & KMOD_CTRL) { + undo(); + } + + if (ev.type == SDL_KEYDOWN && + ev.key.keysym.sym == SDLK_y && + ev.key.keysym.mod & KMOD_CTRL) { + redo(); + } + + if (ev.type == SDL_KEYDOWN && + ev.key.keysym.sym == SDLK_s && + ev.key.keysym.mod & KMOD_CTRL) { + request_save(ev.key.keysym.mod & KMOD_SHIFT); + } + + if (ev.type == SDL_KEYDOWN && + ev.key.keysym.sym == SDLK_o && + ev.key.keysym.mod & KMOD_CTRL) { + MenuManager::instance().set_menu(MenuStorage::PARTICLE_EDITOR_OPEN); + } + +/* + if (!m_enabled) return; + + try + { + if (ev.type == SDL_KEYDOWN && + ev.key.keysym.sym == SDLK_t && + ev.key.keysym.mod & KMOD_CTRL) { + test_level(boost::none); + } + + if (ev.type == SDL_KEYDOWN && + ev.key.keysym.sym == SDLK_s && + ev.key.keysym.mod & KMOD_CTRL) { + save_level(); + } + + if (ev.type == SDL_KEYDOWN && + ev.key.keysym.sym == SDLK_z && + ev.key.keysym.mod & KMOD_CTRL) { + undo(); + } + + if (ev.type == SDL_KEYDOWN && + ev.key.keysym.sym == SDLK_y && + ev.key.keysym.mod & KMOD_CTRL) { + redo(); + } + + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F6) { + Compositor::s_render_lighting = !Compositor::s_render_lighting; + return; + } + + m_ignore_sector_change = false; + + BIND_SECTOR(*m_sector); + + for(const auto& widget : m_controls) { + if (widget->event(ev)) + break; + } + + // unreliable heuristic to snapshot the current state for future undo + if (((ev.type == SDL_KEYUP && ev.key.repeat == 0 && + ev.key.keysym.sym != SDLK_LSHIFT && + ev.key.keysym.sym != SDLK_RSHIFT && + ev.key.keysym.sym != SDLK_LCTRL && + ev.key.keysym.sym != SDLK_RCTRL) || + ev.type == SDL_MOUSEBUTTONUP)) + { + if (!m_ignore_sector_change) { + if (m_level) { + m_undo_manager->try_snapshot(*m_level); + } + } + } + + // Scroll with mouse wheel, if the mouse is not over the toolbox. + // The toolbox does scrolling independently from the main area. + if (ev.type == SDL_MOUSEWHEEL && !m_toolbox_widget->has_mouse_focus() && !m_layers_widget->has_mouse_focus()) { + float scroll_x = static_cast(ev.wheel.x * -32); + float scroll_y = static_cast(ev.wheel.y * -32); + scroll({scroll_x, scroll_y}); + } + } + catch(const std::exception& err) + { + log_warning << "error while processing ParticleEditor::event(): " << err.what() << std::endl; + }*/ +} + +void +ParticleEditor::push_version() +{ + m_redo_stack.clear(); + m_undo_stack.push_back(m_particles->get_props()); +} + +void +ParticleEditor::undo() +{ + if (m_undo_stack.size() < 2) + return; + + m_redo_stack.push_back(m_undo_stack.back()); + m_undo_stack.pop_back(); + m_particles->set_props(m_undo_stack.back()); +} + +void +ParticleEditor::redo() +{ + if (m_redo_stack.size() < 1) + return; + + m_undo_stack.push_back(m_redo_stack.back()); + m_particles->set_props(m_redo_stack.back()); + m_redo_stack.pop_back(); +} + +/* EOF */ diff --git a/src/editor/particle_editor.hpp b/src/editor/particle_editor.hpp new file mode 100644 index 00000000000..34092ceaa34 --- /dev/null +++ b/src/editor/particle_editor.hpp @@ -0,0 +1,120 @@ +// 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 . + +#ifndef HEADER_SUPERTUX_EDITOR_PARTICLE_EDITOR_HPP +#define HEADER_SUPERTUX_EDITOR_PARTICLE_EDITOR_HPP + +#include +#include +#include +#include + +#include "editor/particle_settings_widget.hpp" +#include "interface/control.hpp" +#include "interface/control_button.hpp" +#include "interface/control_enum.hpp" +#include "interface/control_checkbox.hpp" +#include "interface/control_textbox_float.hpp" +#include "interface/control_textbox_int.hpp" +#include "interface/label.hpp" +#include "object/custom_particle_system.hpp" +#include "supertux/screen.hpp" +#include "util/currenton.hpp" +#include "util/writer.hpp" + +class ParticleEditor final : public Screen, + public Currenton +{ + friend class ParticleEditorMenu; + +public: + static bool is_active(); + +public: + ParticleEditor(); + ~ParticleEditor(); + + virtual void draw(Compositor&) override; + virtual void update(float dt_sec, const Controller& controller) override; + + virtual void setup() override; + virtual void leave() override; + + void event(const SDL_Event& ev); + void update_keyboard(const Controller& controller); + void check_unsaved_changes(const std::function& action); + void quit_editor(); + + void reactivate(); + + void open_particle_directory(); + + // saves the particle to file + void save(const std::string& filename, bool retry = false); + void save(Writer& writer); + void request_save(bool is_save_as = false, std::function callback = [](bool was_saved){}); + void open(const std::string& filename) { m_filename = filename; reload(); } + void new_file() { m_filename = ""; reload(); } + + /** Reloads the particle object from the filename in m_filename. + * If m_filename is empty, loads the default file (/particles/default.stcp) + */ + void reload(); + +public: + bool m_enabled; + bool m_quit_request; + +private: + void addTextboxFloat(std::string name, float* bind, + bool (*float_validator)(ControlTextboxFloat*, + float) = nullptr); + void addTextboxFloatWithImprecision(std::string name, float* bind, + float* imprecision_bind, + bool (*float_validator)(ControlTextboxFloat*, + float) = nullptr, + bool (*imp_validator)(ControlTextboxFloat*, + float) = nullptr); + void addTextboxInt(std::string name, int* bind, + bool (*int_validator)(ControlTextboxInt*, + int) = nullptr); + void addCheckbox(std::string name, bool* bind); + void addControl(std::string name, + std::unique_ptr new_control); + void addEasingEnum(std::string name, EasingMode* bind); + + void push_version(); + void undo(); + void redo(); + +private: + std::vector> m_controls; + std::vector> m_undo_stack; + std::vector> m_redo_stack; + + std::shared_ptr m_saved_version; + + CustomParticleSystem* m_particles; + std::string m_filename; + +private: + ParticleEditor(const ParticleEditor&) = delete; + ParticleEditor& operator=(const ParticleEditor&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/editor/particle_settings_widget.cpp b/src/editor/particle_settings_widget.cpp new file mode 100644 index 00000000000..77b0db031d9 --- /dev/null +++ b/src/editor/particle_settings_widget.cpp @@ -0,0 +1,150 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#if 0 +#include "editor/particle_settings_widget.hpp" + +#include + +#include "editor/editor.hpp" +#include "video/drawing_context.hpp" +#include "video/renderer.hpp" +#include "video/video_system.hpp" +#include "video/viewport.hpp" + +ParticleSettingsWidget::ParticleSettingsWidget() : + m_scrolling(), + m_hovering(), + m_total_region(), + m_covered_region(), + m_progress(), + m_rect(), + m_scaled_rect(), + is_horizontal(), + last_mouse_pos(), + zoom_factor() +{ + m_covered_region = VideoSystem::current()->get_viewport().get_rect().get_height(); + m_total_region = 2000; +} + +void +ParticleSettingsWidget::draw(DrawingContext& context) +{ + m_rect = Rect(10, 0, 20, context.get_height()); + + context.color().draw_filled_rect(m_rect, Color(0.5f, 0.5f, 0.5f, 1.f), 8, LAYER_GUI); + context.color().draw_filled_rect(get_bar_rect(), + Color(1.f, 1.f, 1.f, (m_hovering || m_scrolling) ? 1.f : 0.5f), + 8, + LAYER_GUI); +/* + context.color().draw_filled_rect(Rectf(Vector(0, 0), Vector(SIZE, SIZE)), + Color(0.9f, 0.9f, 1.0f, 0.6f), + MIDDLE, LAYER_GUI-10); + context.color().draw_filled_rect(Rectf(Vector(40, 40), Vector(56, 56)), + Color(0.9f, 0.9f, 1.0f, 0.6f), + 8, LAYER_GUI-20); + if (can_scroll()) { + draw_arrow(context, m_mouse_pos); + } + + draw_arrow(context, Vector(TOPLEFT, MIDDLE)); + draw_arrow(context, Vector(BOTTOMRIGHT, MIDDLE)); + draw_arrow(context, Vector(MIDDLE, TOPLEFT)); + draw_arrow(context, Vector(MIDDLE, BOTTOMRIGHT)); +*/ +} + +void +ParticleSettingsWidget::update(float dt_sec) +{ + +} + +bool +ParticleSettingsWidget::on_mouse_button_up(const SDL_MouseButtonEvent& button) +{ + m_scrolling = false; + return false; +} + +bool +ParticleSettingsWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) +{ + if (button.button == SDL_BUTTON_LEFT) { + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (get_bar_rect().contains(int(mouse_pos.x), int(mouse_pos.y))) { + m_scrolling = true; + return true; + } else { + return false; + } + } else { + return false; + } +} + +bool +ParticleSettingsWidget::on_mouse_motion(const SDL_MouseMotionEvent& motion) +{ + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(motion.x, motion.y); + + /*if (mouse_pos.x < SIZE && m_mouse_pos.y < SIZE) { + m_scrolling_vec = m_mouse_pos - Vector(MIDDLE, MIDDLE); + if (m_scrolling_vec.x != 0 || m_scrolling_vec.y != 0) { + float norm = m_scrolling_vec.norm(); + m_scrolling_vec *= powf(static_cast(M_E), norm / 16.0f - 1.0f); + } + }*/ + + m_hovering = get_bar_rect().contains(int(mouse_pos.x), int(mouse_pos.y)); + + int new_progress = m_progress + int((mouse_pos.y - last_mouse_pos) * VideoSystem::current()->get_viewport().get_scale().y * float(m_total_region) / float(m_covered_region)); + last_mouse_pos = mouse_pos.y; + + if (m_scrolling) { + + m_progress = std::min(m_total_region - m_covered_region, std::max(0, new_progress)); + + printf("%d to %d of %d\n", m_progress, m_progress + m_covered_region, m_total_region); + + return true; + } else { + return false; + } +} + +Rect +ParticleSettingsWidget::get_bar_rect() +{ + return Rect(m_rect.left, + m_rect.top + int(float(m_progress) + * float(m_covered_region) + / float(m_total_region) + ), + m_rect.right, + m_rect.top + int(float(m_progress) + * float(m_covered_region) + / float(m_total_region)) + + int(float(m_rect.get_height()) + * float(m_covered_region) + / float(m_total_region) + ) + ); +} +#endif +/* EOF */ diff --git a/src/editor/particle_settings_widget.hpp b/src/editor/particle_settings_widget.hpp new file mode 100644 index 00000000000..8456df12a4d --- /dev/null +++ b/src/editor/particle_settings_widget.hpp @@ -0,0 +1,80 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#if 0 +#ifndef HEADER_SUPERTUX_EDITOR_PARTICLE_SETTINGS_WIDGET_HPP +#define HEADER_SUPERTUX_EDITOR_PARTICLE_SETTINGS_WIDGET_HPP + +#include "editor/widget.hpp" +#include "math/rect.hpp" +#include "math/vector.hpp" + +class DrawingContext; +union SDL_Event; + +/** A generic template for a scrollbar */ +class ParticleSettingsWidget final : public Widget +{ +public: + ParticleSettingsWidget(); + + virtual void draw(DrawingContext& context) override; + virtual void update(float dt_sec) override; + + virtual bool on_mouse_button_up(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override; + +private: + /** Whether or not the mouse is clicking on the bar */ + bool m_scrolling; + + /** Whether or not the mouse hovers above the bar */ + bool m_hovering; + + /** The length (height) of the region to scroll */ + int m_total_region; + + /** The length (height) of the viewport for the region */ + int m_covered_region; + + /** The length (height) between the beginning of the viewport and the beginning of the region */ + int m_progress; + + /** The logical position and size of the widget */ + Rect m_rect; + + /** The position and size of the widget, to scale */ + Rect m_scaled_rect; + + /** `true` of the scroller is horizontal; `false` if it is vertical */ + bool is_horizontal; + +private: + Rect get_bar_rect(); + + float last_mouse_pos; + float zoom_factor; + +private: + ParticleSettingsWidget(const ParticleSettingsWidget&) = delete; + ParticleSettingsWidget& operator=(const ParticleSettingsWidget&) = delete; +}; + +#endif +#endif + +/* EOF */ diff --git a/src/gui/menu_filesystem.cpp b/src/gui/menu_filesystem.cpp index a259f45b114..241a0618f70 100644 --- a/src/gui/menu_filesystem.cpp +++ b/src/gui/menu_filesystem.cpp @@ -28,7 +28,7 @@ #include "util/string_util.hpp" FileSystemMenu::FileSystemMenu(std::string* filename, const std::vector& extensions, - const std::string& basedir) : + const std::string& basedir, std::function callback) : m_filename(filename), // when a basedir is given, 'filename' is relative to basedir, so // it's useless as a starting point @@ -36,7 +36,8 @@ FileSystemMenu::FileSystemMenu(std::string* filename, const std::vectorunmount_old_addons(); @@ -147,6 +148,9 @@ FileSystemMenu::menu_action(MenuItem& item) *m_filename = new_filename; + if (m_callback) + m_callback(*m_filename); + MenuManager::instance().pop_menu(); } else { log_warning << "Selected invalid file or directory" << std::endl; diff --git a/src/gui/menu_filesystem.hpp b/src/gui/menu_filesystem.hpp index 940ce81f0ec..670d625478d 100644 --- a/src/gui/menu_filesystem.hpp +++ b/src/gui/menu_filesystem.hpp @@ -22,7 +22,7 @@ class FileSystemMenu final : public Menu { public: - FileSystemMenu(std::string* filename, const std::vector& extensions, const std::string& basedir); + FileSystemMenu(std::string* filename, const std::vector& extensions, const std::string& basedir, const std::function callback = nullptr); ~FileSystemMenu(); void menu_action(MenuItem& item) override; @@ -38,6 +38,7 @@ class FileSystemMenu final : public Menu std::string m_basedir; std::vector m_directories; std::vector m_files; + std::function m_callback; private: FileSystemMenu(const FileSystemMenu&) = delete; diff --git a/src/interface/control.cpp b/src/interface/control.cpp new file mode 100644 index 00000000000..a1b0081fcd5 --- /dev/null +++ b/src/interface/control.cpp @@ -0,0 +1,26 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "interface/control.hpp" + +InterfaceControl::InterfaceControl() : + m_on_change(), + m_label(), + m_has_focus(), + m_rect() +{ +} + diff --git a/src/interface/control.hpp b/src/interface/control.hpp new file mode 100644 index 00000000000..f454b1d99d3 --- /dev/null +++ b/src/interface/control.hpp @@ -0,0 +1,66 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_INTERFACE_CONTROL_HPP +#define HEADER_SUPERTUX_INTERFACE_CONTROL_HPP + +#include + +#include "control/input_manager.hpp" +#include "editor/widget.hpp" +#include "interface/label.hpp" +#include "video/drawing_context.hpp" + +class InterfaceControl : public Widget +{ +public: + InterfaceControl(); + virtual ~InterfaceControl() {} + + virtual void update(float dt_sec) override { throw std::runtime_error("Cannot call generic update() on interface control"); } + virtual void update(float dt_sec, const Controller& controller) {} + 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; } + + void set_focus(bool focus) { m_has_focus = focus; } + bool has_focus() { return m_has_focus; } + + void set_rect(Rectf rect) { m_rect = rect; } + Rectf get_rect() { return m_rect; } + +public: + /** Optional; a function that will be called each time the bound value + * is modified. + */ + std::function *m_on_change; + + /** Optional; the label associated with the control */ + InterfaceLabel* m_label; + +protected: + /** Whether or not the user has this InterfaceControl as focused */ + bool m_has_focus; + /** The rectangle where the InterfaceControl should be rendered */ + Rectf m_rect; + +private: + InterfaceControl(const InterfaceControl&) = delete; + InterfaceControl& operator=(const InterfaceControl&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/interface/control_button.cpp b/src/interface/control_button.cpp new file mode 100644 index 00000000000..f7ac5b5e734 --- /dev/null +++ b/src/interface/control_button.cpp @@ -0,0 +1,113 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "interface/control_button.hpp" + +#include "math/vector.hpp" +#include "supertux/resources.hpp" +#include "video/video_system.hpp" +#include "video/viewport.hpp" + +ControlButton::ControlButton(std::string label) : + m_btn_label(label), + m_mouse_down(false) +{ +} + +void +ControlButton::draw(DrawingContext& context) +{ + InterfaceControl::draw(context); + + context.color().draw_filled_rect(m_rect, + m_mouse_down ? Color(0.3f, 0.3f, 0.3f, 1.f) : + (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, + 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), + FontAlignment::ALIGN_CENTER, + LAYER_GUI, + Color::BLACK); +} + +bool +ControlButton::on_mouse_button_up(const SDL_MouseButtonEvent& button) +{ + if (button.button == SDL_BUTTON_LEFT) { + + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (m_rect.contains(mouse_pos) && m_mouse_down) { + + if (m_on_change) + (*m_on_change)(); + + m_has_focus = true; + + m_mouse_down = false; + return true; + } else { + m_mouse_down = false; + return false; + } + + } else { + m_mouse_down = false; + return false; + } +} + +bool +ControlButton::on_mouse_button_down(const SDL_MouseButtonEvent& button) +{ + if (button.button == SDL_BUTTON_LEFT) { + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (!m_rect.contains(mouse_pos)) { + m_has_focus = false; + } else { + m_has_focus = true; + m_mouse_down = true; + } + } + return false; +} + +bool +ControlButton::on_key_up(const SDL_KeyboardEvent& key) +{ + if (key.keysym.sym == SDLK_SPACE && m_has_focus) { + if (m_on_change) + (*m_on_change)(); + m_mouse_down = false; + return true; + } else { + return false; + } +} + +bool +ControlButton::on_key_down(const SDL_KeyboardEvent& key) +{ + if (key.keysym.sym == SDLK_SPACE && m_has_focus) { + m_mouse_down = true; + return true; + } else { + return false; + } +} + diff --git a/src/interface/control_button.hpp b/src/interface/control_button.hpp new file mode 100644 index 00000000000..386bdd7327b --- /dev/null +++ b/src/interface/control_button.hpp @@ -0,0 +1,44 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_INTERFACE_CONTROL_BUTTON_HPP +#define HEADER_SUPERTUX_INTERFACE_CONTROL_BUTTON_HPP + +#include "interface/control.hpp" + +class ControlButton : public InterfaceControl +{ +public: + ControlButton(std::string label); + + virtual void draw(DrawingContext& context) override; + virtual bool on_mouse_button_up(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override; + virtual bool on_key_up(const SDL_KeyboardEvent& key) override; + virtual bool on_key_down(const SDL_KeyboardEvent& key) override; + +private: + std::string m_btn_label; + bool m_mouse_down; + +private: + ControlButton(const ControlButton&) = delete; + ControlButton& operator=(const ControlButton&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/interface/control_checkbox.cpp b/src/interface/control_checkbox.cpp new file mode 100644 index 00000000000..49c93c11ebb --- /dev/null +++ b/src/interface/control_checkbox.cpp @@ -0,0 +1,96 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "interface/control_checkbox.hpp" + +#include "math/vector.hpp" +#include "supertux/resources.hpp" +#include "video/video_system.hpp" +#include "video/viewport.hpp" + +ControlCheckbox::ControlCheckbox() : + m_value() +{ +} + +void +ControlCheckbox::draw(DrawingContext& context) +{ + InterfaceControl::draw(context); + + context.color().draw_filled_rect(m_rect, + 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, + "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), + FontAlignment::ALIGN_CENTER, + LAYER_GUI, + Color::BLACK); + } +} + +bool +ControlCheckbox::on_mouse_button_up(const SDL_MouseButtonEvent& button) +{ + if (button.button == SDL_BUTTON_LEFT) { + + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (m_rect.contains(mouse_pos)) { + *m_value = !*m_value; + + if (m_on_change) + (*m_on_change)(); + + m_has_focus = true; + + return true; + } else { + return false; + } + + } else { + return false; + } +} + +bool +ControlCheckbox::on_mouse_button_down(const SDL_MouseButtonEvent& button) +{ + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (!m_rect.contains(mouse_pos)) { + m_has_focus = false; + } + return false; +} + +bool +ControlCheckbox::on_key_up(const SDL_KeyboardEvent& key) +{ + if (key.keysym.sym == SDLK_SPACE && m_has_focus) { + *m_value = !*m_value; + + if (m_on_change) + (*m_on_change)(); + + return true; + } else { + return false; + } +} + diff --git a/src/interface/control_checkbox.hpp b/src/interface/control_checkbox.hpp new file mode 100644 index 00000000000..62a46bb60ec --- /dev/null +++ b/src/interface/control_checkbox.hpp @@ -0,0 +1,46 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_INTERFACE_CONTROL_CHECKBOX_HPP +#define HEADER_SUPERTUX_INTERFACE_CONTROL_CHECKBOX_HPP + +#include "interface/control.hpp" + +class ControlCheckbox : public InterfaceControl +{ +public: + ControlCheckbox(); + + virtual void draw(DrawingContext& context) override; + virtual bool on_mouse_button_up(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override; + virtual bool on_key_up(const SDL_KeyboardEvent& key) override; + + bool get_value() { return *m_value; } + void set_value(bool value) { *m_value = value; } + void bind_value(bool* value) { m_value = value; } + +private: + bool* m_value; + +private: + ControlCheckbox(const ControlCheckbox&) = delete; + ControlCheckbox& operator=(const ControlCheckbox&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/interface/control_enum.cpp b/src/interface/control_enum.cpp new file mode 100644 index 00000000000..8cd582d4299 --- /dev/null +++ b/src/interface/control_enum.cpp @@ -0,0 +1,23 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "interface/control_enum.hpp" + +// The source is in the header file - I can put it in the source file +// because of this bug : + +// https://stackoverflow.com/questions/56041900 + diff --git a/src/interface/control_enum.hpp b/src/interface/control_enum.hpp new file mode 100644 index 00000000000..0db299a640e --- /dev/null +++ b/src/interface/control_enum.hpp @@ -0,0 +1,296 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_INTERFACE_CONTROL_ENUM_HPP +#define HEADER_SUPERTUX_INTERFACE_CONTROL_ENUM_HPP + +#include + +#include "interface/control.hpp" + +template +class ControlEnum : public InterfaceControl +{ +public: + ControlEnum(); + + virtual void draw(DrawingContext& context) override; + virtual bool on_mouse_button_up(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override; + virtual bool on_key_up(const SDL_KeyboardEvent& key) override; + virtual bool on_key_down(const SDL_KeyboardEvent& key) override; + + T get_value() { return *m_value; } + void set_value(T value) { *m_value = value; } + void bind_value(T* value) { m_value = value; } + + void add_option(T key, std::string label) { m_options.insert(std::make_pair(key, label)); } + +private: + T* m_value; + bool m_open_list; + + std::unordered_map m_options; + Vector m_mouse_pos; + +private: + ControlEnum(const ControlEnum&) = delete; + ControlEnum& operator=(const ControlEnum&) = delete; +}; + + + + + +// ============================================================================ +// ============================================================================ +// ============================== SOURCE ================================== +// ============================================================================ +// ============================================================================ + +#include + +#include "math/easing.hpp" +#include "math/vector.hpp" +#include "object/custom_particle_system.hpp" +#include "supertux/resources.hpp" +#include "video/video_system.hpp" +#include "video/viewport.hpp" + +template +ControlEnum::ControlEnum() : + m_value(), + m_open_list(false), + m_options(), + m_mouse_pos() +{ +} + +template +void +ControlEnum::draw(DrawingContext& context) +{ + InterfaceControl::draw(context); + + context.color().draw_filled_rect(m_rect, + m_has_focus ? Color(0.75f, 0.75f, 0.7f, 1.f) + : Color(0.5f, 0.5f, 0.5f, 1.f), + LAYER_GUI); + + std::string label; + auto it = m_options.find(*m_value); + if (it != m_options.end()) { + label = it->second; + } else { + label = ""; + } + + context.color().draw_text(Resources::control_font, + label, + Vector(m_rect.get_left() + 5.f, + (m_rect.get_top() + m_rect.get_bottom()) / 2 - + Resources::control_font->get_height() / 2), + FontAlignment::ALIGN_LEFT, + LAYER_GUI + 1, + Color::BLACK); + int i = 0; + if (m_open_list) { + for (auto option : m_options) { + i++; + Rectf box = m_rect.moved(Vector(0.f, m_rect.get_height() * float(i))); + context.color().draw_filled_rect(box.grown(2.f).moved(Vector(0,4.f)), Color(0.f, 0.f, 0.f, 0.1f), 2.f, LAYER_GUI + 4); + context.color().draw_filled_rect(box.grown(4.f).moved(Vector(0,4.f)), Color(0.f, 0.f, 0.f, 0.1f), 2.f, LAYER_GUI + 4); + context.color().draw_filled_rect(box.grown(6.f).moved(Vector(0,4.f)), Color(0.f, 0.f, 0.f, 0.1f), 2.f, LAYER_GUI + 4); + context.color().draw_filled_rect(box, + (box.contains(m_mouse_pos) + || option.first == *m_value) + ? Color(0.75f, 0.75f, 0.7f, 1.f) + : Color(0.5f, 0.5f, 0.5f, 1.f), + LAYER_GUI + 5); + + std::string label2 = option.second; + + context.color().draw_text(Resources::control_font, + label2, + Vector(m_rect.get_left() + 5.f, + (m_rect.get_top() + m_rect.get_bottom()) / 2 - + Resources::control_font->get_height() / 2 + + m_rect.get_height() * float(i)), + FontAlignment::ALIGN_LEFT, + LAYER_GUI + 6, + Color::BLACK); + } + } +} + +template +bool +ControlEnum::on_mouse_button_up(const SDL_MouseButtonEvent& button) +{ + if (button.button == SDL_BUTTON_LEFT) { + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (m_rect.contains(mouse_pos)) { + m_open_list = !m_open_list; + m_has_focus = true; + return true; + } else if (Rectf(m_rect.get_left(), + m_rect.get_top(), + m_rect.get_right(), + m_rect.get_bottom() + m_rect.get_height() * float(m_options.size()) + ).contains(mouse_pos) && m_open_list) { + return true; + } else { + return false; + } + } else { + return false; + } +} + +template +bool +ControlEnum::on_mouse_button_down(const SDL_MouseButtonEvent& button) +{ + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (m_open_list) { + if (!Rectf(m_rect.get_left(), + m_rect.get_top(), + m_rect.get_right(), + m_rect.get_bottom() + m_rect.get_height() * float(m_options.size()) + ).contains(mouse_pos)) { + m_has_focus = false; + m_open_list = false; + } else { + int pos = int(floor((mouse_pos.y - m_rect.get_bottom()) / m_rect.get_height())); + if (pos != -1) { + // This verification shouldn't be needed but I don't trust myself + if (pos >= 0 && pos < int(m_options.size())) { + // Yes. We need this. Because I can't acced by numerical index. + // There's probably a way, but I'm too bored to investigate. + for (auto option : m_options) { + if (--pos != -1) continue; + *m_value = option.first; + + if (m_on_change) + (*m_on_change)(); + + break; + } + } else { + log_warning << "Clicked on control enum inside dropdown but at invalid position (" + << pos << " for a size of " << m_options.size() << ")" << std::endl; + } + } + return true; + } + } else { + if (!m_rect.contains(mouse_pos)) { + m_has_focus = false; + m_open_list = false; + } + } + return false; +} + +template +bool +ControlEnum::on_mouse_motion(const SDL_MouseMotionEvent& motion) +{ + InterfaceControl::on_mouse_motion(motion); + + m_mouse_pos = VideoSystem::current()->get_viewport().to_logical(motion.x, motion.y); + return false; +} + +template +bool +ControlEnum::on_key_up(const SDL_KeyboardEvent& key) +{ + if ((key.keysym.sym == SDLK_SPACE + || key.keysym.sym == SDLK_RETURN + || key.keysym.sym == SDLK_RETURN2) && m_has_focus) { + m_open_list = !m_open_list; + return true; + } else { + return false; + } +} + +template +bool +ControlEnum::on_key_down(const SDL_KeyboardEvent& key) +{ + if (key.keysym.sym == SDLK_DOWN && m_has_focus) { + bool is_next = false; + // Hacky way to get the next one in the list + for (auto option : m_options) { + if (is_next) { + *m_value = option.first; + is_next = false; + break; + } else if (option.first == *m_value) { + is_next = true; + } + } + + // if we're at the last index, loop back to the beginning + if (is_next) { + // Hacky way to get the first one in the list + for (auto option : m_options) { + *m_value = option.first; + break; + } + } + + if (m_on_change) + (*m_on_change)(); + + return true; + } else if (key.keysym.sym == SDLK_UP && m_has_focus) { + + bool is_last = false; + bool currently_on_first = true; + T last_value = *m_value; // must assign a value else clang will complain + + // Hacky way to get the preceeding one in the list + for (auto option : m_options) { + if (option.first == *m_value) { + if (currently_on_first) { + is_last = true; + } else { + *m_value = last_value; + } + } + last_value = option.first; + currently_on_first = false; + } + + if (is_last) + *m_value = last_value; + + if (m_on_change) + (*m_on_change)(); + + return true; + } else { + return false; + } +} + +#endif + +/* EOF */ diff --git a/src/interface/control_scrollbar.cpp b/src/interface/control_scrollbar.cpp new file mode 100644 index 00000000000..a19b874dc9a --- /dev/null +++ b/src/interface/control_scrollbar.cpp @@ -0,0 +1,151 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "interface/control_scrollbar.hpp" + +#include + +#include "editor/editor.hpp" +#include "video/drawing_context.hpp" +#include "video/renderer.hpp" +#include "video/video_system.hpp" +#include "video/viewport.hpp" + +ControlScrollbar::ControlScrollbar() : + m_scrolling(), + m_hovering(), + m_total_region(), + m_covered_region(), + m_progress(), + m_rect(), + m_scaled_rect(), + //is_horizontal(), + last_mouse_pos() + //zoom_factor() +{ + m_covered_region = VideoSystem::current()->get_viewport().get_rect().get_height(); + m_total_region = 2000; +} + +void +ControlScrollbar::draw(DrawingContext& context) +{ + m_rect = Rect(0, 0, 10, context.get_height()); + + context.color().draw_filled_rect(m_rect, Color(0.5f, 0.5f, 0.5f, 1.f), 8, LAYER_GUI); + context.color().draw_filled_rect(get_bar_rect(), + Color(1.f, 1.f, 1.f, (m_hovering || m_scrolling) ? 1.f : 0.5f), + 8, + LAYER_GUI); +/* + context.color().draw_filled_rect(Rectf(Vector(0, 0), Vector(SIZE, SIZE)), + Color(0.9f, 0.9f, 1.0f, 0.6f), + MIDDLE, LAYER_GUI-10); + context.color().draw_filled_rect(Rectf(Vector(40, 40), Vector(56, 56)), + Color(0.9f, 0.9f, 1.0f, 0.6f), + 8, LAYER_GUI-20); + if (can_scroll()) { + draw_arrow(context, m_mouse_pos); + } + + draw_arrow(context, Vector(TOPLEFT, MIDDLE)); + draw_arrow(context, Vector(BOTTOMRIGHT, MIDDLE)); + draw_arrow(context, Vector(MIDDLE, TOPLEFT)); + draw_arrow(context, Vector(MIDDLE, BOTTOMRIGHT)); +*/ +} + +void +ControlScrollbar::update(float dt_sec) +{ + +} + +bool +ControlScrollbar::on_mouse_button_up(const SDL_MouseButtonEvent& button) +{ + m_scrolling = false; + return false; +} + +bool +ControlScrollbar::on_mouse_button_down(const SDL_MouseButtonEvent& button) +{ + if (button.button == SDL_BUTTON_LEFT) { + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (get_bar_rect().contains(int(mouse_pos.x), int(mouse_pos.y))) { + m_scrolling = true; + return true; + } else { + return false; + } + } else { + return false; + } +} + +bool +ControlScrollbar::on_mouse_motion(const SDL_MouseMotionEvent& motion) +{ + //InterfaceControl::on_mouse_motion(motion); + + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(motion.x, motion.y); + + /*if (mouse_pos.x < SIZE && m_mouse_pos.y < SIZE) { + m_scrolling_vec = m_mouse_pos - Vector(MIDDLE, MIDDLE); + if (m_scrolling_vec.x != 0 || m_scrolling_vec.y != 0) { + float norm = m_scrolling_vec.norm(); + m_scrolling_vec *= powf(static_cast(M_E), norm / 16.0f - 1.0f); + } + }*/ + + m_hovering = get_bar_rect().contains(int(mouse_pos.x), int(mouse_pos.y)); + + int new_progress = m_progress + int((mouse_pos.y - last_mouse_pos) * VideoSystem::current()->get_viewport().get_scale().y * float(m_total_region) / float(m_covered_region)); + last_mouse_pos = mouse_pos.y; + + if (m_scrolling) { + + m_progress = std::min(m_total_region - m_covered_region, std::max(0, new_progress)); + + printf("%d to %d of %d\n", m_progress, m_progress + m_covered_region, m_total_region); + + return true; + } else { + return false; + } +} + +Rect +ControlScrollbar::get_bar_rect() +{ + return Rect(m_rect.left, + m_rect.top + int(float(m_progress) + * float(m_covered_region) + / float(m_total_region) + ), + m_rect.right, + m_rect.top + int(float(m_progress) + * float(m_covered_region) + / float(m_total_region)) + + int(float(m_rect.get_height()) + * float(m_covered_region) + / float(m_total_region) + ) + ); +} + +/* EOF */ diff --git a/src/interface/control_scrollbar.hpp b/src/interface/control_scrollbar.hpp new file mode 100644 index 00000000000..f89b2c86297 --- /dev/null +++ b/src/interface/control_scrollbar.hpp @@ -0,0 +1,78 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_INTERFACE_CONTROL_SCROLLBAR_HPP +#define HEADER_SUPERTUX_INTERFACE_CONTROL_SCROLLBAR_HPP + +#include "editor/widget.hpp" +#include "math/rect.hpp" +#include "math/vector.hpp" + +class DrawingContext; +union SDL_Event; + +/** A generic template for a scrollbar */ +class ControlScrollbar final : public Widget +{ +public: + ControlScrollbar(); + + virtual void draw(DrawingContext& context) override; + virtual void update(float dt_sec) override; + + virtual bool on_mouse_button_up(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override; + +private: + /** Whether or not the mouse is clicking on the bar */ + bool m_scrolling; + + /** Whether or not the mouse hovers above the bar */ + bool m_hovering; + + /** The length (height) of the region to scroll */ + int m_total_region; + + /** The length (height) of the viewport for the region */ + int m_covered_region; + + /** The length (height) between the beginning of the viewport and the beginning of the region */ + int m_progress; + + /** The logical position and size of the widget */ + Rect m_rect; + + /** The position and size of the widget, to scale */ + Rect m_scaled_rect; + + /** `true` of the scroller is horizontal; `false` if it is vertical */ + //bool is_horizontal; + +private: + Rect get_bar_rect(); + + float last_mouse_pos; + //float zoom_factor; + +private: + ControlScrollbar(const ControlScrollbar&) = delete; + ControlScrollbar& operator=(const ControlScrollbar&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/interface/control_textbox.cpp b/src/interface/control_textbox.cpp new file mode 100644 index 00000000000..02ea1ccb660 --- /dev/null +++ b/src/interface/control_textbox.cpp @@ -0,0 +1,457 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "interface/control_textbox.hpp" + +#include + +#include "math/vector.hpp" +#include "math/rectf.hpp" +#include "supertux/resources.hpp" +#include "video/video_system.hpp" +#include "video/viewport.hpp" + +// Shamelessly snatched values from src/gui/menu.cpp +static const float CONTROL_REPEAT_INITIAL = 0.4f; +static const float CONTROL_REPEAT_RATE = 0.05f; +static const float CONTROL_CURSOR_TIMER = 0.5f; + +ControlTextbox::ControlTextbox() : + m_validate_string(), + m_charlist(), + m_string(nullptr), + m_internal_string_backup(""), + m_backspace_remaining(CONTROL_REPEAT_INITIAL), + m_cursor_timer(CONTROL_CURSOR_TIMER), + m_caret_pos(), + m_secondary_caret_pos(), + m_shift_pressed(false), + m_mouse_pressed(), + m_current_offset(0) +{ +} + +void +ControlTextbox::update(float dt_sec, const Controller& controller) +{ + if (controller.pressed(Control::REMOVE)) { + on_backspace(); + } + + if (controller.hold(Control::REMOVE)) { + m_backspace_remaining -= dt_sec; + while (m_backspace_remaining < 0) { + on_backspace(); + m_backspace_remaining += CONTROL_REPEAT_RATE; + } + } else { + m_backspace_remaining = CONTROL_REPEAT_INITIAL; + } + + m_cursor_timer -= dt_sec; + if (m_cursor_timer < -CONTROL_CURSOR_TIMER) { + m_cursor_timer = CONTROL_CURSOR_TIMER; + } + + // Apparently the stuff bugs from time to time + recenter_offset(); +} + +void +ControlTextbox::on_backspace() +{ + if (m_charlist.size()) { + if (m_caret_pos != m_secondary_caret_pos) { + + m_cursor_timer = CONTROL_CURSOR_TIMER; + + auto it = m_charlist.begin(); + advance(it, std::min(m_caret_pos, m_secondary_caret_pos)); + auto it2 = m_charlist.begin(); + advance(it2, std::max(m_caret_pos, m_secondary_caret_pos)); + m_charlist.erase(it, it2); + + m_caret_pos = std::min(m_caret_pos, m_secondary_caret_pos); + m_secondary_caret_pos = m_caret_pos; + + } else if (m_caret_pos > 0) { + m_caret_pos--; + m_secondary_caret_pos = m_caret_pos; + m_cursor_timer = CONTROL_CURSOR_TIMER; + + auto it = m_charlist.begin(); + advance(it, m_caret_pos); + m_charlist.erase(it); + } + + recenter_offset(); + } +} + +void +ControlTextbox::draw(DrawingContext& context) +{ + InterfaceControl::draw(context); + + context.color().draw_filled_rect(m_rect, + 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_caret_pos != m_secondary_caret_pos) { + float lgt1 = Resources::control_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 + ->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()) + ))); + + context.color().draw_filled_rect(Rectf(m_rect.p1() + Vector(lgt1 + 5.f, 0.f), + m_rect.p1() + Vector(lgt2 + 5.f, m_rect.get_height()) + ), + m_has_focus ? Color(1.f, 1.f, .9f, 0.75f) + : Color(1.f, 1.f, .9f, 0.5f), + LAYER_GUI); + } + + context.color().draw_text(Resources::control_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), + FontAlignment::ALIGN_LEFT, + LAYER_GUI + 1, + Color::BLACK); + if (m_cursor_timer > 0 && m_has_focus) { + float lgt = Resources::control_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), + Color::BLACK, + LAYER_GUI + 1); + } +} + +bool +ControlTextbox::on_mouse_button_down(const SDL_MouseButtonEvent& button) +{ + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (m_rect.contains(mouse_pos)) { + m_has_focus = true; + m_cursor_timer = CONTROL_CURSOR_TIMER; + m_caret_pos = get_text_position(mouse_pos); + m_secondary_caret_pos = m_caret_pos; + m_mouse_pressed = true; + return true; + } else { + if (m_has_focus) { + parse_value(); + } + m_has_focus = false; + } + return false; +} + +bool +ControlTextbox::on_mouse_button_up(const SDL_MouseButtonEvent& button) +{ + if (m_mouse_pressed) { + m_mouse_pressed = false; + return true; + } + return false; +} + +bool +ControlTextbox::on_mouse_motion(const SDL_MouseMotionEvent& motion) +{ + InterfaceControl::on_mouse_motion(motion); + + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(motion.x, motion.y); + if (m_mouse_pressed) { + m_cursor_timer = CONTROL_CURSOR_TIMER; + m_caret_pos = get_text_position(mouse_pos); + return true; + } + return false; +} + +bool +ControlTextbox::on_key_up(const SDL_KeyboardEvent& key) +{ + + if (m_has_focus && + (key.keysym.sym == SDLK_LSHIFT || key.keysym.sym == SDLK_RSHIFT)) { + + m_shift_pressed = false; + return true; + + } + + return false; +} + +bool +ControlTextbox::on_key_down(const SDL_KeyboardEvent& key) +{ + + if (m_has_focus && key.keysym.sym == SDLK_LEFT + && m_caret_pos > 0) { + + m_caret_pos--; + m_cursor_timer = CONTROL_CURSOR_TIMER; + if (!m_shift_pressed) { + m_secondary_caret_pos = m_caret_pos; + } + + recenter_offset(); + return true; + + } else if (m_has_focus && key.keysym.sym == SDLK_RIGHT + && m_caret_pos < int(m_charlist.size())) { + + m_caret_pos++; + m_cursor_timer = CONTROL_CURSOR_TIMER; + if (!m_shift_pressed) { + m_secondary_caret_pos = m_caret_pos; + } + + recenter_offset(); + return true; + + } else if (m_has_focus && key.keysym.sym == SDLK_HOME) { + + m_caret_pos = 0; + m_cursor_timer = CONTROL_CURSOR_TIMER; + if (!m_shift_pressed) { + m_secondary_caret_pos = m_caret_pos; + } + + recenter_offset(); + return true; + + } else if (m_has_focus && key.keysym.sym == SDLK_END) { + + m_caret_pos = int(m_charlist.size()); + m_cursor_timer = CONTROL_CURSOR_TIMER; + if (!m_shift_pressed) { + m_secondary_caret_pos = m_caret_pos; + } + + recenter_offset(); + return true; + + } else if (m_has_focus && + (key.keysym.sym == SDLK_LSHIFT || key.keysym.sym == SDLK_RSHIFT)) { + + m_shift_pressed = true; + return true; + + } else if (m_has_focus && key.keysym.sym == SDLK_RETURN) { + + parse_value(); + return true; + + } + + return false; +} + +bool +ControlTextbox::event(const SDL_Event& ev) { + Widget::event(ev); + + if (ev.type == SDL_TEXTINPUT && m_has_focus) { + + if (m_secondary_caret_pos != m_caret_pos) { + m_cursor_timer = CONTROL_CURSOR_TIMER; + + auto it = m_charlist.begin(); + advance(it, std::min(m_caret_pos, m_secondary_caret_pos)); + auto it2 = m_charlist.begin(); + advance(it2, std::max(m_caret_pos, m_secondary_caret_pos)); + m_charlist.erase(it, it2); + + m_caret_pos = std::min(m_caret_pos, m_secondary_caret_pos); + m_secondary_caret_pos = m_caret_pos; + } + + auto it = m_charlist.begin(); + advance(it, m_caret_pos); + m_charlist.insert(it, ev.text.text[0]); + + m_caret_pos++; + m_secondary_caret_pos = m_caret_pos; + m_cursor_timer = CONTROL_CURSOR_TIMER; + + recenter_offset(); + } + + return false; +} + +bool +ControlTextbox::parse_value(bool call_on_change /* = true (see header)*/) +{ + // 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())) { + 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)(); + } + + return true; +} + +void +ControlTextbox::revert_value() +{ + std::string str = m_internal_string_backup; + + m_charlist.clear(); + for (char c : str) { + m_charlist.push_back(c); + } + + m_caret_pos = 0; + m_secondary_caret_pos = 0; + + recenter_offset(); +} + +std::string +ControlTextbox::get_string() +{ + return m_internal_string_backup; +} + +std::string +ControlTextbox::get_contents() +{ + std::string temp; + + for (char c : m_charlist) { + temp += c; + } + temp += '\0'; + + return temp; +} + +std::string +ControlTextbox::get_first_chars(int amount) +{ + std::string temp; + + for (char c : m_charlist) { + if (!(amount--)) break; + temp += c; + } + temp += '\0'; + + return temp; +} + +std::string +ControlTextbox::get_contents_visible() +{ + std::string temp; + int remaining = m_current_offset; + + for (char c : m_charlist) { + if (--remaining < 0) { + temp += c; + } + } + temp += '\0'; + + return get_truncated_text(temp); +} + +std::string +ControlTextbox::get_first_chars_visible(int amount) +{ + return get_contents_visible().substr(0, amount); +} + +int +ControlTextbox::get_text_position(Vector pos) +{ + float dist = pos.x - m_rect.get_left(); + int i = 0; + + while (Resources::control_font->get_text_width(get_first_chars_visible(i)) < dist + && i <= int(m_charlist.size())) + i++; + + return std::max(i - 1 + m_current_offset, 0); +} + +std::string +ControlTextbox::get_truncated_text(std::string text) +{ + if (fits(text)) return text; + + std::string temp = text; + while (!temp.empty() && !fits(temp)) + temp.pop_back(); + + return temp; +} + +bool +ControlTextbox::fits(std::string text) +{ + return Resources::control_font->get_text_width(text) <= m_rect.get_width() - 10.f; +} + +void +ControlTextbox::recenter_offset() +{ + 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())) { + m_current_offset++; + } + + while (m_current_offset > 0 && fits(get_contents().substr(m_current_offset - 1))) { + m_current_offset--; + } +} + +/* EOF */ diff --git a/src/interface/control_textbox.hpp b/src/interface/control_textbox.hpp new file mode 100644 index 00000000000..966a44b6a27 --- /dev/null +++ b/src/interface/control_textbox.hpp @@ -0,0 +1,162 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_INTERFACE_CONTROL_TEXTBOX_HPP +#define HEADER_SUPERTUX_INTERFACE_CONTROL_TEXTBOX_HPP + +#include + +#include "control/input_manager.hpp" +#include "interface/control.hpp" + + +class ControlTextbox : public InterfaceControl +{ +public: + ControlTextbox(); + + virtual void draw(DrawingContext& context) override; + virtual bool on_mouse_button_up(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override; + virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override; + virtual bool on_key_up(const SDL_KeyboardEvent& key) override; + virtual bool on_key_down(const SDL_KeyboardEvent& key) override; + virtual bool event(const SDL_Event& ev) override; + + virtual void update(float dt_sec, const Controller& controller) override; + + /** Binds a string to the textbox */ + void bind_string(std::string* value) { m_string = value; } + + /** Returns the full string held in m_charlist */ + std::string get_string(); + + /** Gets at which (absolute) index, in the text, corresponds an on-screen point */ + int get_text_position(Vector pos); + + /** Returns true if the given text would fit inside the box */ + bool fits(std::string text); + +protected: + /** Transfers the string into the binded variable, if any. Can be overridden + * by children if they use different value members (like float, int, etc). + * parse_value MUST make the validation, and MUST use get_contents() to + * fetch the string to parse, as get_string() will return the precedent + * valid string. + * @param call_on_change Whether calling this function should also call + * m_on_change(). Children classes that implement + * another m_value member (float, int, etc.) should + * call `ControlTextbox::parse_value(false)` to parse + * the string without calling m_on_change immediately; + * the child class should call m_on_change() by itself + * when and only when they have parsed their value. + * @returns Whether or not the value in the textbox was a valid value. If + * it isn't, then parse_value() should handle reverting the char + * vector to its last valid state. + * @see get_string() + * @see revert_value() + */ + virtual bool parse_value(bool call_on_change = true); + + /** Reverts the contents of the char vector to the value of the member + * variable. Can be overridden by children, as parse_value() does. + */ + virtual void revert_value(); + +protected: + /** WARNING : These function returns the status of the string *as the user + * types*, and therefore are unsuited for callers expecting a validated + * value. If such case is, use get_string() instead. If you use these + * functions below, you'll get a half-typed value! (And don't validate + * while the user is typing, it could make typing impossible!) + */ + + /** Converts the internal char vector to an actual string and returns it. */ + std::string get_contents(); + + /** Returns first "amount" chars held in m_charlist */ + std::string get_first_chars(int amount); + + /** Returns the part of the string that is actually displayed */ + std::string get_contents_visible(); + + /** Returns first "amount" chars that are displayed */ + std::string get_first_chars_visible(int amount); + +public: + /** Optional, a function to validate the string. If nullptr, then all values + * are assumed to be valid. + * + * @param ControlTextbox* A pointer to the original control. + * @param std::string The string that is about to be applied to the textbox. + * @returns Whether or not this value is valid. If not, then the internal + * string will not be modified. Tip : You can manually change the + * string using set_string() inside this function and return false + * to establish a custom value (for example, a max string length). + */ + bool (*m_validate_string)(ControlTextbox*, std::string); + +protected: + /** Holds the list of characters that are in the textbox. When characters are + * added or deleted, this is the member variable that is getting edited. + */ + std::list m_charlist; + + /** This is the value that should be looked at by external functions. + * @see get_string() + * @see bind_string() + * @see m_internal_string_backup + */ + std::string* m_string; + + /** Used so that if m_string is not bound, get_string() won't break/segfault + * @see get_string() + * @see bind_string() + * @see m_string + */ + std::string m_internal_string_backup; + + float m_backspace_remaining; + float m_cursor_timer; + int m_caret_pos; + int m_secondary_caret_pos; /**< Used for selections */ + bool m_shift_pressed; + bool m_mouse_pressed; + + /** + * If the string is too long to be contained in the box, + * use this offset to select which characters will be + * displayed on the screen + */ + int m_current_offset; + +protected: + void on_backspace(); + + /** Returns the largest string fitting in the box. */ + std::string get_truncated_text(std::string text); + + /** Changes m_current_offset so the the caret is visible */ + void recenter_offset(); + +private: + ControlTextbox(const ControlTextbox&) = delete; + ControlTextbox& operator=(const ControlTextbox&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/interface/control_textbox_float.cpp b/src/interface/control_textbox_float.cpp new file mode 100644 index 00000000000..6567ad21bfb --- /dev/null +++ b/src/interface/control_textbox_float.cpp @@ -0,0 +1,95 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "interface/control_textbox_float.hpp" + +#include +#include + +ControlTextboxFloat::ControlTextboxFloat() : + m_validate_float(), + m_value() +{ + float value = 0.f; + m_value = &value; + revert_value(); +} + +void +ControlTextboxFloat::update(float dt_sec, const Controller& controller) +{ + ControlTextbox::update(dt_sec, controller); + if (!m_has_focus) + revert_value(); +} + +bool +ControlTextboxFloat::parse_value(bool call_on_change /* = true (see header */) +{ + // Calling super will put the correct value in m_string. + if (!ControlTextbox::parse_value(false)) { + // If the parent has failed, abandon. Keeping parsing should still result + // in the parsing of a correct value (get_string() will return the last + // valid value), but it would be unnecessary, since the last valid value + // is already the one that's currently displayed. + return false; + } + + float temp; + try { + temp = std::stof(get_contents()); + } catch (std::exception&) { + revert_value(); + return false; + } + + if (m_validate_float) { + if (!m_validate_float(this, temp)) { + revert_value(); + return false; + } + } + + if (*m_value != temp) { + *m_value = temp; + + // Do it anyways + revert_value(); + + if (call_on_change && m_on_change) + (*m_on_change)(); + } + + return true; +} + +void +ControlTextboxFloat::revert_value() +{ + m_internal_string_backup = std::to_string(*m_value); + + // Remove the trailing zeroes at the end of the decimal point... + while (m_internal_string_backup.at(m_internal_string_backup.size() - 1) == '0') + m_internal_string_backup.pop_back(); + + // ...but keep at least one number after the point + if (m_internal_string_backup.at(m_internal_string_backup.size() - 1) == '.') + m_internal_string_backup += "0"; + + ControlTextbox::revert_value(); +} + +/* EOF */ diff --git a/src/interface/control_textbox_float.hpp b/src/interface/control_textbox_float.hpp new file mode 100644 index 00000000000..56dc7cca172 --- /dev/null +++ b/src/interface/control_textbox_float.hpp @@ -0,0 +1,66 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_INTERFACE_CONTROL_TEXTBOX_FLOAT_HPP +#define HEADER_SUPERTUX_INTERFACE_CONTROL_TEXTBOX_FLOAT_HPP + +#include "interface/control_textbox.hpp" + +class ControlTextboxFloat : public ControlTextbox +{ +public: + ControlTextboxFloat(); + + virtual void update(float dt_sec, const Controller& controller) override; + + float get_value() { return *m_value; } + void set_value(float value) { *m_value = value; revert_value(); } + /** Binds a float to this textbox. Set m_validate_float(float) if you want + * custom validation. (You may also use m_validate_string(string), though + * it's not recommended) + * @param value A pointer to the value to be bound. MUST NOT BE NULL (FIXME) + */ + void bind_value(float* value) { m_value = value; revert_value(); } + +protected: + virtual bool parse_value(bool call_on_change = true) override; + virtual void revert_value() override; + +public: + /** Optional, a function to validate the float. If nullptr, then all values + * are assumed to be valid. + * + * @param ControlTextboxFloat* A pointer to the original control. + * @param float The float that is about to be applied to the textbox. + * @returns Whether or not this value is valid. If not, then the internal + * values will not be modified. Tip : You can manually change the + * values using set_value() inside this function and return false + * to establish a custom value (for example, a max float value). + * @see m_validate_string If you want to validate using a string instead. + */ + bool (*m_validate_float)(ControlTextboxFloat*, float); + +private: + float* m_value; + +private: + ControlTextboxFloat(const ControlTextboxFloat&) = delete; + ControlTextboxFloat& operator=(const ControlTextboxFloat&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/interface/control_textbox_int.cpp b/src/interface/control_textbox_int.cpp new file mode 100644 index 00000000000..585cf7e2523 --- /dev/null +++ b/src/interface/control_textbox_int.cpp @@ -0,0 +1,87 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "interface/control_textbox_int.hpp" + +#include +#include + +ControlTextboxInt::ControlTextboxInt() : + m_validate_int(), + m_value() +{ + int value = 0; + m_value = &value; + revert_value(); +} + +void +ControlTextboxInt::update(float dt_sec, const Controller& controller) +{ + ControlTextbox::update(dt_sec, controller); + if (!m_has_focus) + revert_value(); +} + +bool +ControlTextboxInt::parse_value(bool call_on_change /* = true (see header */) +{ + // Calling super will put the correct value in m_string. + if (!ControlTextbox::parse_value(false)) { + // If the parent has failed, abandon. Keeping parsing should still result + // in the parsing of a correct value (get_string() will return the last + // valid value), but it would be unnecessary, since the last valid value + // is already the one that's currently displayed. + return false; + } + + int temp; + try { + temp = std::stoi(get_contents()); + } catch (std::exception&) { + revert_value(); + return false; + } + + if (m_validate_int) { + if (!m_validate_int(this, temp)) { + revert_value(); + return false; + } + } + + if (*m_value != temp) { + *m_value = temp; + + // Do it anyways + revert_value(); + + if (call_on_change && m_on_change) + (*m_on_change)(); + } + + return true; +} + +void +ControlTextboxInt::revert_value() +{ + m_internal_string_backup = std::to_string(*m_value); + ControlTextbox::revert_value(); +} + + +/* EOF */ diff --git a/src/interface/control_textbox_int.hpp b/src/interface/control_textbox_int.hpp new file mode 100644 index 00000000000..f7b58939db3 --- /dev/null +++ b/src/interface/control_textbox_int.hpp @@ -0,0 +1,66 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_INTERFACE_CONTROL_TEXTBOX_INT_HPP +#define HEADER_SUPERTUX_INTERFACE_CONTROL_TEXTBOX_INT_HPP + +#include "interface/control_textbox.hpp" + +class ControlTextboxInt : public ControlTextbox +{ +public: + ControlTextboxInt(); + + virtual void update(float dt_sec, const Controller& controller) override; + + int get_value() { return *m_value; } + void set_value(int value) { *m_value = value; revert_value(); } + /** Binds an int to this textbox. Set m_validate_fint(int) if you want + * custom validation. (You may also use m_validate_string(string), though + * it's not recommended) + * @param value A pointer to the value to be bound. MUST NOT BE NULL (FIXME) + */ + void bind_value(int* value) { m_value = value; revert_value(); } + +protected: + virtual bool parse_value(bool call_on_change = true) override; + virtual void revert_value() override; + +public: + /** Optional, a function to validate the integer. If nullptr, then all values + * are assumed to be valid. + * + * @param ControlTextboxInt* A pointer to the original control. + * @param int The integer that is about to be applied to the textbox. + * @returns Whether or not this value is valid. If not, then the internal + * values will not be modified. Tip : You can manually change the + * values using set_value() inside this function and return false + * to establish a custom value (for example, a max float value). + * @see m_validate_string If you want to validate using a string instead. + */ + bool (*m_validate_int)(ControlTextboxInt*, int); + +private: + int* m_value; + +private: + ControlTextboxInt(const ControlTextboxInt&) = delete; + ControlTextboxInt& operator=(const ControlTextboxInt&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/interface/label.cpp b/src/interface/label.cpp new file mode 100644 index 00000000000..7fc85e38955 --- /dev/null +++ b/src/interface/label.cpp @@ -0,0 +1,98 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "interface/label.hpp" + +#include "supertux/resources.hpp" +#include "video/video_system.hpp" +#include "video/viewport.hpp" + +InterfaceLabel::InterfaceLabel() : + m_rect(), + m_label(), + m_mouse_pos() +{ +} + +InterfaceLabel::InterfaceLabel(Rectf rect, std::string label) : + m_rect(rect), + m_label(label), + m_mouse_pos() +{ +} + +bool +InterfaceLabel::on_mouse_motion(const SDL_MouseMotionEvent& motion) +{ + m_mouse_pos = VideoSystem::current()->get_viewport().to_logical(motion.x, motion.y); + return false; +} + +void +InterfaceLabel::draw(DrawingContext& context) +{ + context.color().draw_text(Resources::control_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), + 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, + m_mouse_pos + Vector(0, 33.f), + FontAlignment::ALIGN_LEFT, + LAYER_GUI + 11, + Color::WHITE); + } +} + +bool +InterfaceLabel::fits(std::string text) +{ + return Resources::control_font->get_text_width(text) <= m_rect.get_width(); +} + +std::string +InterfaceLabel::get_truncated_text() +{ + if (fits(m_label)) return m_label; + + std::string temp = m_label; + while (!temp.empty() && !fits(temp + "...")) + temp.pop_back(); + + return temp + "..."; +} + + diff --git a/src/interface/label.hpp b/src/interface/label.hpp new file mode 100644 index 00000000000..8d2043ac1df --- /dev/null +++ b/src/interface/label.hpp @@ -0,0 +1,60 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_INTERFACE_LABEL_HPP +#define HEADER_SUPERTUX_INTERFACE_LABEL_HPP + +#include + +#include "editor/widget.hpp" +#include "video/drawing_context.hpp" + +class InterfaceLabel : public Widget +{ +public: + InterfaceLabel(); + InterfaceLabel(Rectf rect, std::string label); + virtual ~InterfaceLabel() {} + + virtual void draw(DrawingContext& context) override; + virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override; + + void set_rect(Rectf rect) { m_rect = rect; } + Rectf get_rect() { return m_rect; } + + void set_label(std::string label) { m_label = label; } + std::string get_label() { return m_label; } + + bool fits(std::string text); + std::string get_truncated_text(); + +protected: + /** The rectangle where the InterfaceLabel should be rendered */ + Rectf m_rect; + /** The text of the label */ + std::string m_label; + +private: + Vector m_mouse_pos; + +private: + InterfaceLabel(const InterfaceLabel&) = delete; + InterfaceLabel& operator=(const InterfaceLabel&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/object/custom_particle_system.cpp b/src/object/custom_particle_system.cpp index 5797d3ad210..21fc9c52e63 100644 --- a/src/object/custom_particle_system.cpp +++ b/src/object/custom_particle_system.cpp @@ -20,13 +20,16 @@ #include #include "collision/collision.hpp" +#include "editor/particle_editor.hpp" +#include "gui/menu_manager.hpp" #include "math/aatriangle.hpp" #include "math/easing.hpp" #include "math/random.hpp" #include "object/camera.hpp" -#include "object/particle_zone.hpp" #include "object/tilemap.hpp" +#include "supertux/fadetoblack.hpp" #include "supertux/game_session.hpp" +#include "supertux/screen_manager.hpp" #include "supertux/sector.hpp" #include "supertux/tile.hpp" #include "util/reader.hpp" @@ -78,7 +81,7 @@ CustomParticleSystem::CustomParticleSystem() : m_particle_offscreen_mode(), m_cover_screen(true) { - init(); + reinit_textures(); } CustomParticleSystem::CustomParticleSystem(const ReaderMapping& reader) : @@ -284,7 +287,7 @@ CustomParticleSystem::CustomParticleSystem(const ReaderMapping& reader) : m_particle_offscreen_mode = OffscreenMode::Never; } - init(); + reinit_textures(); } CustomParticleSystem::~CustomParticleSystem() @@ -292,10 +295,10 @@ CustomParticleSystem::~CustomParticleSystem() } void -CustomParticleSystem::init() +CustomParticleSystem::reinit_textures() { + m_textures.clear(); // TODO: Multiple textures for a single particle system object? - // TODO: Handle color and scale multipliers per-texture //m_textures.push_back(SpriteProperties(Surface::from_file("images/engine/editor/sparkle.png"))); //for (float r = 0.f; r < 1.f; r += 0.5f) { @@ -426,6 +429,8 @@ CustomParticleSystem::get_settings() //result.reorder({"amount", "delay", "lifetime", "lifetime-variation", "enabled", "name"}); + result.add_particle_editor(); + return result; } @@ -523,7 +528,7 @@ CustomParticleSystem::update(float dt_sec) particle->speedX *= 1.f - particle->frictionX * dt_sec; particle->speedY *= 1.f - particle->frictionY * dt_sec; - if (collision(particle, + if (Sector::current() && collision(particle, Vector(particle->speedX,particle->speedY) * dt_sec) > 0) { switch(particle->collision_mode) { case CollisionMode::Ignore: @@ -563,8 +568,8 @@ CustomParticleSystem::update(float dt_sec) particle->angle += particle->angle_speed * dt_sec; } - float abs_x = Sector::get().get_camera().get_translation().x; - float abs_y = Sector::get().get_camera().get_translation().y; + float abs_x = get_abs_x(); + float abs_y = get_abs_y(); if (!particle->has_been_on_screen) { if (particle->pos.y <= static_cast(SCREEN_HEIGHT) + abs_y @@ -598,7 +603,7 @@ CustomParticleSystem::update(float dt_sec) } bool is_in_life_zone = false; - for (auto& zone : GameSession::current()->get_current_sector().get_objects_by_type()) { + for (auto& zone : get_zones()) { if (zone.get_rect().contains(particle->pos) && zone.get_particle_name() == m_name) { switch(zone.get_type()) { case ParticleZone::ParticleZoneType::Killer: @@ -640,14 +645,16 @@ CustomParticleSystem::update(float dt_sec) } // For each particle + // Clear dead particles // Scroll through the vector backwards, because removing an element affects // the index of all elements after it (prevents buggy behavior) for (int i = static_cast(custom_particles.size()) - 1; i >= 0; --i) { auto particle = dynamic_cast(custom_particles.at(i).get()); - if (particle->ready_for_deletion) + if (particle->ready_for_deletion) { custom_particles.erase(custom_particles.begin()+i); + } } // Add necessary particles @@ -657,7 +664,7 @@ CustomParticleSystem::update(float dt_sec) int real_max = m_max_amount; if (!m_cover_screen) { int i = 0; - for (auto& zone : GameSession::current()->get_current_sector().get_objects_by_type()) { + for (auto& zone : get_zones()) { if (zone.get_type() == ParticleZone::ParticleZoneType::Spawn && zone.get_particle_name() == m_name) { i++; } @@ -838,6 +845,47 @@ CustomParticleSystem::get_random_texture() return m_textures.at(0); } +std::vector +CustomParticleSystem::get_zones() +{ + std::vector list; + + //if (!!GameSession::current() && Sector::current()) { + if (!ParticleEditor::current()) { + + // In game or in level editor + for (auto& zone : GameSession::current()->get_current_sector().get_objects_by_type()) { + list.push_back(zone.get_details()); + } + + } else { + + // In particle editor + list.push_back(ParticleZone::ZoneDetails(m_name, + ParticleZone::ParticleZoneType::Spawn, + Rectf(virtual_width / 2 - 16.f, + virtual_height / 2 - 16.f, + virtual_width / 2 + 16.f, + virtual_height / 2 + 16.f) + )); + + } + + return list; +} + +float +CustomParticleSystem::get_abs_x() +{ + return (Sector::current()) ? Sector::get().get_camera().get_translation().x : 0.f; +} + +float +CustomParticleSystem::get_abs_y() +{ + return (Sector::current()) ? Sector::get().get_camera().get_translation().y : 0.f; +} + /** Initializes and adds a single particle to the stack. Performs * no check regarding the maximum amount of total particles. * @param lifetime The time elapsed since the moment the particle should have been born @@ -918,7 +966,7 @@ void CustomParticleSystem::spawn_particles(float lifetime) { if (!m_cover_screen) { - for (auto& zone : GameSession::current()->get_current_sector().get_objects_by_type()) { + for (auto& zone : get_zones()) { if (zone.get_type() == ParticleZone::ParticleZoneType::Spawn && zone.get_particle_name() == m_name) { Rectf rect = zone.get_rect(); add_particle(lifetime, @@ -927,8 +975,8 @@ CustomParticleSystem::spawn_particles(float lifetime) } } } else { - float abs_x = Sector::get().get_camera().get_translation().x; - float abs_y = Sector::get().get_camera().get_translation().y; + float abs_x = get_abs_x(); + float abs_y = get_abs_y(); add_particle(lifetime, graphicsRandom.randf(virtual_width) + abs_x, graphicsRandom.randf(virtual_height) + abs_y); diff --git a/src/object/custom_particle_system.hpp b/src/object/custom_particle_system.hpp index 1c14f87c85a..33f5095f4f4 100644 --- a/src/object/custom_particle_system.hpp +++ b/src/object/custom_particle_system.hpp @@ -20,14 +20,16 @@ #include "math/easing.hpp" #include "math/vector.hpp" #include "object/particlesystem_interactive.hpp" +#include "object/particle_zone.hpp" #include "scripting/custom_particles.hpp" #include "video/surface.hpp" #include "video/surface_ptr.hpp" -class CustomParticleSystem final : +class CustomParticleSystem : public ParticleSystem_Interactive, public ExposedObject { + friend class ParticleEditor; public: CustomParticleSystem(); CustomParticleSystem(const ReaderMapping& reader); @@ -35,7 +37,7 @@ class CustomParticleSystem final : virtual void draw(DrawingContext& context) override; - void init(); + void reinit_textures(); virtual void update(float dt_sec) override; virtual std::string get_class() const override { return "particles-custom"; } @@ -55,11 +57,18 @@ class CustomParticleSystem final : // Local void add_particle(float lifetime, float x, float y); void spawn_particles(float lifetime); + + std::vector get_zones(); + + float get_abs_x(); + float get_abs_y(); float texture_sum_odds; float time_last_remaining; +public: // Scripting + void clear() { custom_particles.clear(); } private: enum class RotationMode { @@ -95,7 +104,7 @@ class CustomParticleSystem final : Color color; SurfacePtr texture; Vector scale; - + SpriteProperties() : likeliness(1.f), color(1.f, 1.f, 1.f, 1.f), @@ -103,7 +112,7 @@ class CustomParticleSystem final : scale(1.f, 1.f) { } - + SpriteProperties(SurfacePtr surface) : likeliness(1.f), color(1.f, 1.f, 1.f, 1.f), @@ -111,7 +120,7 @@ class CustomParticleSystem final : scale(1.f, 1.f) { } - + SpriteProperties(SpriteProperties& sp, float alpha) : likeliness(sp.likeliness), color(sp.color.red, sp.color.green, sp.color.blue, sp.color.alpha * alpha), @@ -225,6 +234,162 @@ class CustomParticleSystem final : OffscreenMode m_particle_offscreen_mode; bool m_cover_screen; +public: + // TODO: Put all those member variables in some (abstract?) class of which + // both CustomParticlesSystem and ParticleProbs will inherit (so that + // I don't have to write all the variables 4 times just in the header) + + // For the particle editor + class ParticleProps final + { + public: + std::string m_particle_main_texture; + int m_max_amount; + float m_delay; + float m_particle_lifetime; + float m_particle_lifetime_variation; + float m_particle_birth_time, + m_particle_birth_time_variation, + m_particle_death_time, + m_particle_death_time_variation; + FadeMode m_particle_birth_mode, + m_particle_death_mode; + EasingMode m_particle_birth_easing, + m_particle_death_easing; + float m_particle_speed_x, + m_particle_speed_y, + m_particle_speed_variation_x, + m_particle_speed_variation_y, + m_particle_acceleration_x, + m_particle_acceleration_y, + m_particle_friction_x, + m_particle_friction_y; + float m_particle_feather_factor; + float m_particle_rotation, + m_particle_rotation_variation, + m_particle_rotation_speed, + m_particle_rotation_speed_variation, + m_particle_rotation_acceleration, + m_particle_rotation_decceleration; + RotationMode m_particle_rotation_mode; + CollisionMode m_particle_collision_mode; + OffscreenMode m_particle_offscreen_mode; + bool m_cover_screen; + + ParticleProps() : + m_particle_main_texture(), + m_max_amount(25), + m_delay(0.1f), + m_particle_lifetime(5.f), + m_particle_lifetime_variation(0.f), + m_particle_birth_time(0.f), + m_particle_birth_time_variation(0.f), + m_particle_death_time(0.f), + m_particle_death_time_variation(0.f), + m_particle_birth_mode(), + m_particle_death_mode(), + m_particle_birth_easing(), + m_particle_death_easing(), + m_particle_speed_x(0.f), + m_particle_speed_y(0.f), + m_particle_speed_variation_x(0.f), + m_particle_speed_variation_y(0.f), + m_particle_acceleration_x(0.f), + m_particle_acceleration_y(0.f), + m_particle_friction_x(0.f), + m_particle_friction_y(0.f), + m_particle_feather_factor(0.f), + m_particle_rotation(0.f), + m_particle_rotation_variation(0.f), + m_particle_rotation_speed(0.f), + m_particle_rotation_speed_variation(0.f), + m_particle_rotation_acceleration(0.f), + m_particle_rotation_decceleration(0.f), + m_particle_rotation_mode(), + m_particle_collision_mode(), + m_particle_offscreen_mode(), + m_cover_screen(true) + { + } + }; + + std::shared_ptr get_props() const + { + std::shared_ptr props = std::make_shared(); + + props->m_particle_main_texture = m_particle_main_texture; + props->m_max_amount = m_max_amount; + props->m_delay = m_delay; + props->m_particle_lifetime = m_particle_lifetime; + props->m_particle_lifetime_variation = m_particle_lifetime_variation; + props->m_particle_birth_time = m_particle_birth_time; + props->m_particle_birth_time_variation = m_particle_birth_time_variation; + props->m_particle_death_time = m_particle_death_time; + props->m_particle_death_time_variation = m_particle_death_time_variation; + props->m_particle_birth_mode = m_particle_birth_mode; + props->m_particle_death_mode = m_particle_death_mode; + props->m_particle_birth_easing = m_particle_birth_easing; + props->m_particle_death_easing = m_particle_death_easing; + props->m_particle_speed_x = m_particle_speed_x; + props->m_particle_speed_y = m_particle_speed_y; + props->m_particle_speed_variation_x = m_particle_speed_variation_x; + props->m_particle_speed_variation_y = m_particle_speed_variation_y; + props->m_particle_acceleration_x = m_particle_acceleration_x; + props->m_particle_acceleration_y = m_particle_acceleration_y; + props->m_particle_friction_x = m_particle_friction_x; + props->m_particle_friction_y = m_particle_friction_y; + props->m_particle_feather_factor = m_particle_feather_factor; + props->m_particle_rotation = m_particle_rotation; + props->m_particle_rotation_variation = m_particle_rotation_variation; + props->m_particle_rotation_speed = m_particle_rotation_speed; + props->m_particle_rotation_speed_variation = m_particle_rotation_speed_variation; + props->m_particle_rotation_acceleration = m_particle_rotation_acceleration; + props->m_particle_rotation_decceleration = m_particle_rotation_decceleration; + props->m_particle_rotation_mode = m_particle_rotation_mode; + props->m_particle_collision_mode = m_particle_collision_mode; + props->m_particle_offscreen_mode = m_particle_offscreen_mode; + props->m_cover_screen = m_cover_screen; + + return props; + } + + void set_props(std::shared_ptr props) + { + m_particle_main_texture = props->m_particle_main_texture; + m_max_amount = props->m_max_amount; + m_delay = props->m_delay; + m_particle_lifetime = props->m_particle_lifetime; + m_particle_lifetime_variation = props->m_particle_lifetime_variation; + m_particle_birth_time = props->m_particle_birth_time; + m_particle_birth_time_variation = props->m_particle_birth_time_variation; + m_particle_death_time = props->m_particle_death_time; + m_particle_death_time_variation = props->m_particle_death_time_variation; + m_particle_birth_mode = props->m_particle_birth_mode; + m_particle_death_mode = props->m_particle_death_mode; + m_particle_birth_easing = props->m_particle_birth_easing; + m_particle_death_easing = props->m_particle_death_easing; + m_particle_speed_x = props->m_particle_speed_x; + m_particle_speed_y = props->m_particle_speed_y; + m_particle_speed_variation_x = props->m_particle_speed_variation_x; + m_particle_speed_variation_y = props->m_particle_speed_variation_y; + m_particle_acceleration_x = props->m_particle_acceleration_x; + m_particle_acceleration_y = props->m_particle_acceleration_y; + m_particle_friction_x = props->m_particle_friction_x; + m_particle_friction_y = props->m_particle_friction_y; + m_particle_feather_factor = props->m_particle_feather_factor; + m_particle_rotation = props->m_particle_rotation; + m_particle_rotation_variation = props->m_particle_rotation_variation; + m_particle_rotation_speed = props->m_particle_rotation_speed; + m_particle_rotation_speed_variation = props->m_particle_rotation_speed_variation; + m_particle_rotation_acceleration = props->m_particle_rotation_acceleration; + m_particle_rotation_decceleration = props->m_particle_rotation_decceleration; + m_particle_rotation_mode = props->m_particle_rotation_mode; + m_particle_collision_mode = props->m_particle_collision_mode; + m_particle_offscreen_mode = props->m_particle_offscreen_mode; + m_cover_screen = props->m_cover_screen; + } + + private: CustomParticleSystem(const CustomParticleSystem&) = delete; CustomParticleSystem& operator=(const CustomParticleSystem&) = delete; diff --git a/src/object/custom_particle_system_file.cpp b/src/object/custom_particle_system_file.cpp new file mode 100644 index 00000000000..35a77e7e612 --- /dev/null +++ b/src/object/custom_particle_system_file.cpp @@ -0,0 +1,72 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 "object/custom_particle_system_file.hpp" + +#include "gui/menu_manager.hpp" +#include "util/reader.hpp" +#include "util/reader_document.hpp" +#include "util/reader_mapping.hpp" + +CustomParticleSystemFile::CustomParticleSystemFile() : + CustomParticleSystem(), + m_filename() +{ +} + +CustomParticleSystemFile::CustomParticleSystemFile(const ReaderMapping& reader) : + CustomParticleSystem(reader), + m_filename() +{ + reader.get("file", m_filename, "default.stcp"); + + update_data(); + reinit_textures(); +} + +CustomParticleSystemFile::~CustomParticleSystemFile() +{ +} + +ObjectSettings +CustomParticleSystemFile::get_settings() +{ + ObjectSettings result = ParticleSystem::get_settings(); + + result.add_file(_("File"), &m_filename, "file", {}, {".stcp"}, "/particles"); + result.add_particle_editor(); + + result.add_remove(); + + return result; +} + +void +CustomParticleSystemFile::update_data() +{ + // FIXME: Add a try-catch in case of I/O error (in case we switch to a dialog + // that doesn't filter strictly) + auto doc = ReaderDocument::from_file("particles/" + ((m_filename == "") ? "default.stcp" : m_filename)); + auto root = doc.get_root(); + auto mapping = root.get_mapping(); + + if (root.get_name() != "supertux-custom-particle") + throw std::runtime_error("file is not a supertux-custom-particle file."); + + set_props(CustomParticleSystem(mapping).get_props()); +} + +/* EOF */ diff --git a/src/object/custom_particle_system_file.hpp b/src/object/custom_particle_system_file.hpp new file mode 100644 index 00000000000..8e883839075 --- /dev/null +++ b/src/object/custom_particle_system_file.hpp @@ -0,0 +1,59 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_OBJECT_CUSTOM_PARTICLE_SYSTEM_FILE_HPP +#define HEADER_SUPERTUX_OBJECT_CUSTOM_PARTICLE_SYSTEM_FILE_HPP + +#include "math/easing.hpp" +#include "math/vector.hpp" +#include "object/custom_particle_system.hpp" +#include "object/particlesystem_interactive.hpp" +#include "object/particle_zone.hpp" +#include "scripting/custom_particles.hpp" +#include "video/surface.hpp" +#include "video/surface_ptr.hpp" + +class CustomParticleSystemFile final : + public CustomParticleSystem +{ + friend class ParticleEditor; +public: + CustomParticleSystemFile(); + CustomParticleSystemFile(const ReaderMapping& reader); + virtual ~CustomParticleSystemFile(); + + virtual std::string get_class() const override { return "particles-custom-file"; } + virtual std::string get_display_name() const override { return _("Custom Particles from file"); } + virtual ObjectSettings get_settings() override; + + virtual const std::string get_icon_path() const override { + return "images/engine/editor/sparkle-file.png"; + } + +private: + void update_data(); + +private: + std::string m_filename; + +private: + CustomParticleSystemFile(const CustomParticleSystemFile&) = delete; + CustomParticleSystemFile& operator=(const CustomParticleSystemFile&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/object/particle_zone.cpp b/src/object/particle_zone.cpp index 6f72ace59ed..c1602e08a83 100644 --- a/src/object/particle_zone.cpp +++ b/src/object/particle_zone.cpp @@ -17,6 +17,7 @@ #include "object/particle_zone.hpp" #include "editor/editor.hpp" +#include "supertux/resources.hpp" #include "util/reader_mapping.hpp" #include "video/drawing_context.hpp" @@ -99,28 +100,33 @@ void ParticleZone::draw(DrawingContext& context) { if (Editor::is_active()) { + Color c; switch(m_type) { case ParticleZoneType::Spawn: - context.color().draw_filled_rect(m_col.m_bbox, Color(0.5f, 0.5f, 1.0f, 0.6f), - 0.0f, LAYER_OBJECTS); + c = Color(0.5f, 0.5f, 1.0f, 0.6f); break; case ParticleZoneType::Life: - context.color().draw_filled_rect(m_col.m_bbox, Color(0.5f, 1.0f, 0.5f, 0.6f), - 0.0f, LAYER_OBJECTS); + c = Color(0.5f, 1.0f, 0.5f, 0.6f); break; case ParticleZoneType::LifeClear: - context.color().draw_filled_rect(m_col.m_bbox, Color(1.0f, 1.0f, 0.5f, 0.6f), - 0.0f, LAYER_OBJECTS); + c = Color(1.0f, 1.0f, 0.5f, 0.6f); break; case ParticleZoneType::Killer: - context.color().draw_filled_rect(m_col.m_bbox, Color(1.0f, 0.75f, 0.5f, 0.6f), - 0.0f, LAYER_OBJECTS); + c = Color(1.0f, 0.75f, 0.5f, 0.6f); break; case ParticleZoneType::Destroyer: - context.color().draw_filled_rect(m_col.m_bbox, Color(1.0f, 0.5f, 0.5f, 0.6f), - 0.0f, LAYER_OBJECTS); + c = Color(1.0f, 0.5f, 0.5f, 0.6f); break; } + + context.color().draw_filled_rect(m_col.m_bbox, c, + 0.0f, LAYER_OBJECTS); + context.color().draw_text(Resources::small_font, + m_particle_name, + m_col.m_bbox.p1(), + FontAlignment::ALIGN_LEFT, + LAYER_GUI + 2, + Color::WHITE); } } diff --git a/src/object/particle_zone.hpp b/src/object/particle_zone.hpp index a904d1671b5..f9361cab4a4 100644 --- a/src/object/particle_zone.hpp +++ b/src/object/particle_zone.hpp @@ -46,9 +46,9 @@ class ParticleZone final : enum class ParticleZoneType { /** Particles will spawn in this area */ Spawn, - /** TODO: Particles will die if they leave this area */ + /** Particles will die if they leave this area */ Life, - /** TODO: Particles will disappear instantly if they leave this area */ + /** Particles will disappear instantly if they leave this area */ LifeClear, /** Particles will start dying if they touch this area */ Killer, @@ -98,6 +98,26 @@ class ParticleZone final : void set_type(ParticleZoneType type) {m_type = type;} ParticleZoneType get_type() {return m_type;} + class ZoneDetails { + public: + std::string m_particle_name; + ParticleZoneType m_type; + Rectf m_rect; + + ZoneDetails(std::string name, ParticleZoneType type, Rectf rect) : + m_particle_name(name), + m_type(type), + m_rect(rect) + { + } + + Rectf get_rect() const {return m_rect;} + ParticleZoneType get_type() const {return m_type;} + std::string get_particle_name() const {return m_particle_name;} + }; + + ZoneDetails get_details() { return ZoneDetails(m_particle_name, m_type, m_col.m_bbox); } + private: bool m_enabled; std::string m_particle_name; diff --git a/src/physfs/ifile_streambuf.cpp b/src/physfs/ifile_streambuf.cpp index b47ad6c763d..5e28bc1e8b0 100644 --- a/src/physfs/ifile_streambuf.cpp +++ b/src/physfs/ifile_streambuf.cpp @@ -34,7 +34,7 @@ IFileStreambuf::IFileStreambuf(const std::string& filename) : if (file == nullptr) { std::stringstream msg; msg << "Couldn't open file '" << filename << "': " - << PHYSFS_getLastErrorCode(); + << PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()); throw std::runtime_error(msg.str()); } } diff --git a/src/physfs/ofile_streambuf.cpp b/src/physfs/ofile_streambuf.cpp index 0bae4de96e4..967f3673796 100644 --- a/src/physfs/ofile_streambuf.cpp +++ b/src/physfs/ofile_streambuf.cpp @@ -27,7 +27,7 @@ OFileStreambuf::OFileStreambuf(const std::string& filename) : if (file == nullptr) { std::stringstream msg; msg << "Couldn't open file '" << filename << "': " - << PHYSFS_getLastErrorCode(); + << PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()); throw std::runtime_error(msg.str()); } diff --git a/src/scripting/custom_particles.cpp b/src/scripting/custom_particles.cpp index e5db41b2e7b..32b34a3f610 100644 --- a/src/scripting/custom_particles.cpp +++ b/src/scripting/custom_particles.cpp @@ -49,6 +49,12 @@ void CustomParticles::set_amount(int amount, float time) //object.fade_amount(amount, time); } +void CustomParticles::clear() +{ + SCRIPT_GUARD_VOID; + object.clear(); +} + } // namespace scripting /* EOF */ diff --git a/src/scripting/custom_particles.hpp b/src/scripting/custom_particles.hpp index 2f06c927fcd..62129b95ae6 100644 --- a/src/scripting/custom_particles.hpp +++ b/src/scripting/custom_particles.hpp @@ -51,6 +51,9 @@ class CustomParticles final /** Smoothly changes the amount of particles to the given value */ void set_amount(int amount, float time); + + /** Instantly removes all particles of that type on the screen */ + void clear(); }; } // namespace scripting diff --git a/src/scripting/wrapper.cpp b/src/scripting/wrapper.cpp index cd010bbf395..26ac23b7b0f 100644 --- a/src/scripting/wrapper.cpp +++ b/src/scripting/wrapper.cpp @@ -1033,6 +1033,35 @@ static SQInteger CustomParticles_set_amount_wrapper(HSQUIRRELVM vm) } +static SQInteger CustomParticles_clear_wrapper(HSQUIRRELVM vm) +{ + SQUserPointer data; + if(SQ_FAILED(sq_getinstanceup(vm, 1, &data, nullptr)) || !data) { + sq_throwerror(vm, _SC("'clear' called without instance")); + return SQ_ERROR; + } + auto _this = reinterpret_cast (data); + + if (_this == nullptr) { + return SQ_ERROR; + } + + + try { + _this->clear(); + + return 0; + + } catch(std::exception& e) { + sq_throwerror(vm, e.what()); + return SQ_ERROR; + } catch(...) { + sq_throwerror(vm, _SC("Unexpected exception while executing function 'clear'")); + return SQ_ERROR; + } + +} + static SQInteger Decal_release_hook(SQUserPointer ptr, SQInteger ) { auto _this = reinterpret_cast (ptr); @@ -8914,6 +8943,13 @@ void register_supertux_wrapper(HSQUIRRELVM v) throw SquirrelError(v, "Couldn't register function 'set_amount'"); } + sq_pushstring(v, "clear", -1); + sq_newclosure(v, &CustomParticles_clear_wrapper, 0); + sq_setparamscheck(v, SQ_MATCHTYPEMASKSTRING, "x|t"); + if(SQ_FAILED(sq_createslot(v, -3))) { + throw SquirrelError(v, "Couldn't register function 'clear'"); + } + if(SQ_FAILED(sq_createslot(v, -3))) { throw SquirrelError(v, "Couldn't register class 'CustomParticles'"); } diff --git a/src/supertux/colorscheme.cpp b/src/supertux/colorscheme.cpp index ed46150048b..b3e8894d990 100644 --- a/src/supertux/colorscheme.cpp +++ b/src/supertux/colorscheme.cpp @@ -17,6 +17,7 @@ #include "supertux/colorscheme.hpp" #include "editor/overlay_widget.hpp" +#include "interface/control.hpp" #include "object/floating_text.hpp" #include "object/level_time.hpp" #include "object/text_object.hpp" diff --git a/src/supertux/error_handler.cpp b/src/supertux/error_handler.cpp new file mode 100644 index 00000000000..a06dff1f1c9 --- /dev/null +++ b/src/supertux/error_handler.cpp @@ -0,0 +1,77 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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/error_handler.hpp" + +// apparently this is exclusive to glibc as of 2020, keep checking for +// llvm/msvc equivalent from time to time ~ Semphris +#ifdef __GLIBCXX__ +#include +#include +#include +#endif + +bool ErrorHandler::m_handing_error = false; + +void +ErrorHandler::set_handlers() +{ +#ifdef __GLIBCXX__ + signal(SIGSEGV, handle_error); +#endif +} + +[[ noreturn ]] void +ErrorHandler::handle_error(int sig) +{ + if (m_handing_error) + { + // Error happened again while handling another segfault. Abort now. + close_program(); + } + else + { + m_handing_error = true; + // Do not use external stuff (like log_fatal) to limit the risk of causing + // another error, which would restart the handler again + fprintf(stderr, "\nError: signal %d:\n", sig); + print_stack_trace(); + close_program(); + } +} + +void +ErrorHandler::print_stack_trace() +{ +#ifdef __GLIBCXX__ + void *array[10]; + size_t size; + + // get void*'s for all entries on the stack + size = backtrace(array, 10); + + // print out all the frames to stderr + backtrace_symbols_fd(array, static_cast(size), STDERR_FILENO); +#endif +} + +[[ noreturn ]] void +ErrorHandler::close_program() +{ + exit(10); +} + +/* EOF */ diff --git a/src/supertux/error_handler.hpp b/src/supertux/error_handler.hpp new file mode 100644 index 00000000000..6d2f7c3080c --- /dev/null +++ b/src/supertux/error_handler.hpp @@ -0,0 +1,45 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_SUPERTUX_ERROR_HANDLER_HPP +#define HEADER_SUPERTUX_SUPERTUX_ERROR_HANDLER_HPP + +#include + +class ErrorHandler final +{ +public: + static void set_handlers(); + +private: + [[ noreturn ]] static void handle_error(int sig); + + static void print_stack_trace(); + [[ noreturn ]] static void close_program(); + +private: + static bool m_handing_error; + +private: + ErrorHandler() = delete; + ~ErrorHandler() = delete; + ErrorHandler(const ErrorHandler&) = delete; + ErrorHandler& operator=(const ErrorHandler&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/supertux/game_object_factory.cpp b/src/supertux/game_object_factory.cpp index ecd1ace16af..27d9fb2ea72 100644 --- a/src/supertux/game_object_factory.cpp +++ b/src/supertux/game_object_factory.cpp @@ -80,6 +80,7 @@ #include "object/circleplatform.hpp" #include "object/cloud_particle_system.hpp" #include "object/custom_particle_system.hpp" +#include "object/custom_particle_system_file.hpp" #include "object/coin.hpp" #include "object/decal.hpp" #include "object/explosion.hpp" @@ -215,6 +216,7 @@ GameObjectFactory::init_factories() add_factory("circleplatform"); add_factory("particles-clouds"); add_factory("particles-custom"); + add_factory("particles-custom-file"); add_factory("coin"); add_factory("decal"); add_factory("explosion"); diff --git a/src/supertux/main.cpp b/src/supertux/main.cpp index 36a9e527d13..c91167336d4 100644 --- a/src/supertux/main.cpp +++ b/src/supertux/main.cpp @@ -52,6 +52,7 @@ extern "C" { #include "sprite/sprite_manager.hpp" #include "supertux/command_line_arguments.hpp" #include "supertux/console.hpp" +#include "supertux/error_handler.hpp" #include "supertux/game_manager.hpp" #include "supertux/game_session.hpp" #include "supertux/gameconfig.hpp" @@ -538,6 +539,9 @@ Main::launch_game(const CommandLineArguments& args) int Main::run(int argc, char** argv) { + // First and foremost, set error handlers (to print stack trace on SIGSEGV, etc.) + ErrorHandler::set_handlers(); + #ifdef WIN32 //SDL is used instead of PHYSFS because both create the same path in app data //However, PHYSFS is not yet initizlized, and this should be run before anything is initialized diff --git a/src/supertux/menu/menu_storage.cpp b/src/supertux/menu/menu_storage.cpp index d934145ad35..0208abbc62a 100644 --- a/src/supertux/menu/menu_storage.cpp +++ b/src/supertux/menu/menu_storage.cpp @@ -38,6 +38,9 @@ #include "supertux/menu/language_menu.hpp" #include "supertux/menu/main_menu.hpp" #include "supertux/menu/options_menu.hpp" +#include "supertux/menu/particle_editor_menu.hpp" +#include "supertux/menu/particle_editor_save_as.hpp" +#include "supertux/menu/particle_editor_open.hpp" #include "supertux/menu/profile_menu.hpp" #include "supertux/menu/worldmap_menu.hpp" #include "supertux/menu/worldmap_cheat_menu.hpp" @@ -156,6 +159,16 @@ MenuStorage::create(MenuId menu_id) case EDITOR_LEVELSET_MENU: return std::make_unique(); + case PARTICLE_EDITOR_MENU: + return std::make_unique(); + + case PARTICLE_EDITOR_SAVE_AS: + throw new std::runtime_error("Cannot instantiate ParticleEditorSaveAs dialog from MenuStorage::create() or MenuManager::set_menu(), as it needs to be bound to a callback. Please instantiate ParticleEditorSaveAs directly"); + //return std::make_unique(); + + case PARTICLE_EDITOR_OPEN: + return std::make_unique(); + case NO_MENU: return std::unique_ptr(); diff --git a/src/supertux/menu/menu_storage.hpp b/src/supertux/menu/menu_storage.hpp index 7df75596698..f87044ca22e 100644 --- a/src/supertux/menu/menu_storage.hpp +++ b/src/supertux/menu/menu_storage.hpp @@ -63,7 +63,10 @@ class MenuStorage final EDITOR_SECTORS_MENU, EDITOR_SECTOR_MENU, EDITOR_LEVEL_MENU, - EDITOR_LEVELSET_MENU + EDITOR_LEVELSET_MENU, + PARTICLE_EDITOR_MENU, + PARTICLE_EDITOR_SAVE_AS, + PARTICLE_EDITOR_OPEN }; public: diff --git a/src/supertux/menu/particle_editor_menu.cpp b/src/supertux/menu/particle_editor_menu.cpp new file mode 100644 index 00000000000..c3360edd1d6 --- /dev/null +++ b/src/supertux/menu/particle_editor_menu.cpp @@ -0,0 +1,136 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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/particle_editor_menu.hpp" + +#include "editor/particle_editor.hpp" +#include "gui/dialog.hpp" +#include "gui/menu_filesystem.hpp" +#include "gui/menu_item.hpp" +#include "gui/menu_manager.hpp" +#include "supertux/level.hpp" +#include "supertux/gameconfig.hpp" +#include "supertux/menu/menu_storage.hpp" +#include "util/gettext.hpp" +#include "video/compositor.hpp" + +ParticleEditorMenu::ParticleEditorMenu() +{ + add_label(_("Particle Editor")); + + add_hl(); + + add_entry(MNID_RETURNTOEDITOR, _("Return to Editor")); + add_entry(MNID_NEW, _("New Particle Config")); + add_entry(MNID_SAVE, _("Save Particle Config")); + add_entry(MNID_SAVE_AS, _("Save Particle Config as...")); + add_entry(MNID_LOAD, _("Load Another Particle Config")); + + add_hl(); + + add_entry(MNID_OPEN_DIR, _("Open Particle Directory")); + add_entry(MNID_HELP, _("Keyboard Shortcuts")); + + add_hl(); + + add_entry(MNID_QUITEDITOR, _("Exit Particle Editor")); +} + +ParticleEditorMenu::~ParticleEditorMenu() +{ + auto editor = ParticleEditor::current(); + if (editor == nullptr) { + return; + } + editor->reactivate(); +} + +void +ParticleEditorMenu::menu_action(MenuItem& item) +{ + switch (item.get_id()) + { + case MNID_RETURNTOEDITOR: + MenuManager::instance().clear_menu_stack(); + break; + + case MNID_NEW: + { + MenuManager::instance().clear_menu_stack(); + auto editor = ParticleEditor::current(); + editor->check_unsaved_changes([editor] { + editor->new_file(); + }); + } + break; + + case MNID_SAVE: + { + MenuManager::instance().clear_menu_stack(); + auto editor = ParticleEditor::current(); + editor->request_save(); + } + break; + + case MNID_SAVE_AS: + { + MenuManager::instance().clear_menu_stack(); + auto editor = ParticleEditor::current(); + editor->request_save(true); + } + break; + + case MNID_OPEN_DIR: + ParticleEditor::current()->open_particle_directory(); + break; + + case MNID_LOAD: + //MenuManager::instance().set_menu(MenuStorage::PARTICLE_EDITOR_OPEN); + { + const std::vector& filter = {".stcp"}; + MenuManager::instance().push_menu(std::make_unique( + &ParticleEditor::current()->m_filename, + filter, + "/particles/custom", + [](std::string new_filename) { + ParticleEditor::current()->open("/particles/custom/" + + ParticleEditor::current()->m_filename); + MenuManager::instance().clear_menu_stack(); + } + )); + } + break; + + case MNID_HELP: + { + auto dialog = std::make_unique(); + dialog->set_text(_("Keyboard Shortcuts:\n---------------------\nEsc = Open Menu\nCtrl+S = Save\nCtrl+Shift+S = Save as\nCtrl+O = Open\nCtrl+Z = Undo\nCtrl+Y = Redo")); + dialog->add_cancel_button(_("Got it!")); + MenuManager::instance().set_dialog(std::move(dialog)); + } + break; + + case MNID_QUITEDITOR: + MenuManager::instance().clear_menu_stack(); + ParticleEditor::current()->m_quit_request = true; + break; + + default: + break; + } +} + +/* EOF */ diff --git a/src/supertux/menu/particle_editor_menu.hpp b/src/supertux/menu/particle_editor_menu.hpp new file mode 100644 index 00000000000..6becb671e4d --- /dev/null +++ b/src/supertux/menu/particle_editor_menu.hpp @@ -0,0 +1,49 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +#ifndef HEADER_SUPERTUX_SUPERTUX_MENU_PARTICLE_EDITOR_MENU_HPP +#define HEADER_SUPERTUX_SUPERTUX_MENU_PARTICLE_EDITOR_MENU_HPP + +#include "gui/menu.hpp" + +class ParticleEditorMenu final : public Menu +{ +private: + enum MenuIDs { + MNID_RETURNTOEDITOR, + MNID_NEW, + MNID_SAVE, + MNID_SAVE_AS, + MNID_LOAD, + MNID_OPEN_DIR, + MNID_HELP, + MNID_QUITEDITOR + }; + +public: + ParticleEditorMenu(); + ~ParticleEditorMenu(); + + void menu_action(MenuItem& item) override; + +private: + ParticleEditorMenu(const ParticleEditorMenu&) = delete; + ParticleEditorMenu& operator=(const ParticleEditorMenu&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/supertux/menu/particle_editor_open.cpp b/src/supertux/menu/particle_editor_open.cpp new file mode 100644 index 00000000000..b19a4397843 --- /dev/null +++ b/src/supertux/menu/particle_editor_open.cpp @@ -0,0 +1,74 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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/particle_editor_open.hpp" + +#include "editor/particle_editor.hpp" +#include "gui/dialog.hpp" +#include "gui/menu_item.hpp" +#include "gui/menu_manager.hpp" +#include "supertux/level.hpp" +#include "supertux/gameconfig.hpp" +#include "supertux/menu/menu_storage.hpp" +#include "util/gettext.hpp" +#include "video/compositor.hpp" + +ParticleEditorOpen::ParticleEditorOpen() : + m_filename() +{ + add_label(_("Load particle file")); + + add_hl(); + + std::vector extensions; + extensions.push_back("stcp"); + add_file(_("File"), &m_filename, extensions, "/particles/custom"); + add_entry(MNID_OPEN, _("Open")); + + add_hl(); + + add_entry(MNID_CANCEL, _("Cancel")); +} + +ParticleEditorOpen::~ParticleEditorOpen() +{ + auto editor = ParticleEditor::current(); + if (editor == nullptr) { + return; + } + editor->reactivate(); +} + +void +ParticleEditorOpen::menu_action(MenuItem& item) +{ + switch (item.get_id()) + { + case MNID_OPEN: + ParticleEditor::current()->open("/particles/custom/" + m_filename); + MenuManager::instance().clear_menu_stack(); + break; + + case MNID_CANCEL: + MenuManager::instance().clear_menu_stack(); + break; + + default: + break; + } +} + +/* EOF */ diff --git a/src/supertux/menu/particle_editor_open.hpp b/src/supertux/menu/particle_editor_open.hpp new file mode 100644 index 00000000000..3e1ad95cc94 --- /dev/null +++ b/src/supertux/menu/particle_editor_open.hpp @@ -0,0 +1,48 @@ +// SuperTux +// Copyright (C) 2020 A. Semphris +// +// 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 . + +// TODO: DEPRECATED (Remove hpp, cpp, and references in the menu manager) + +#ifndef HEADER_SUPERTUX_SUPERTUX_MENU_PARTICLE_EDITOR_OPEN_HPP +#define HEADER_SUPERTUX_SUPERTUX_MENU_PARTICLE_EDITOR_OPEN_HPP + +#include "gui/menu.hpp" + +class ParticleEditorOpen final : public Menu +{ +private: + enum MenuIDs { + MNID_OPEN, + MNID_CANCEL + }; + +public: + ParticleEditorOpen(); + ~ParticleEditorOpen(); + + void menu_action(MenuItem& item) override; + +private: + std::string m_filename; + +private: + ParticleEditorOpen(const ParticleEditorOpen&) = delete; + ParticleEditorOpen& operator=(const ParticleEditorOpen&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/supertux/menu/particle_editor_save_as.cpp b/src/supertux/menu/particle_editor_save_as.cpp new file mode 100644 index 00000000000..8448c0e675e --- /dev/null +++ b/src/supertux/menu/particle_editor_save_as.cpp @@ -0,0 +1,78 @@ +// 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/particle_editor_save_as.hpp" + +#include "editor/particle_editor.hpp" +#include "gui/dialog.hpp" +#include "gui/menu_item.hpp" +#include "gui/menu_manager.hpp" +#include "supertux/level.hpp" +#include "supertux/gameconfig.hpp" +#include "supertux/menu/menu_storage.hpp" +#include "util/gettext.hpp" +#include "video/compositor.hpp" + +ParticleEditorSaveAs::ParticleEditorSaveAs(std::function callback) : + m_filename("/particles/custom/"), + m_callback(callback) +{ + add_label(_("Save particle as")); + + add_hl(); + + add_textfield(_("File name"), &m_filename); + add_entry(MNID_SAVE, _("Save")); + + add_hl(); + + add_entry(MNID_CANCEL, _("Cancel")); +} + +ParticleEditorSaveAs::~ParticleEditorSaveAs() +{ + auto editor = ParticleEditor::current(); + if (editor == nullptr) { + return; + } + editor->reactivate(); +} + +void +ParticleEditorSaveAs::menu_action(MenuItem& item) +{ + switch (item.get_id()) + { + case MNID_SAVE: + ParticleEditor::current()->save(m_filename); + // In this very case, if you clear the dialog stack before calling the + // callback, the callback will lose its reference to the Particle Editor, + // which will cause a segfault. Somehow. Somebody explain me. ~Semphris + m_callback(true); + MenuManager::instance().clear_menu_stack(); + break; + + case MNID_CANCEL: + m_callback(false); + MenuManager::instance().clear_menu_stack(); + break; + + default: + break; + } +} + +/* EOF */ diff --git a/src/supertux/menu/particle_editor_save_as.hpp b/src/supertux/menu/particle_editor_save_as.hpp new file mode 100644 index 00000000000..7058d4ba08f --- /dev/null +++ b/src/supertux/menu/particle_editor_save_as.hpp @@ -0,0 +1,47 @@ +// 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 . + +#ifndef HEADER_SUPERTUX_SUPERTUX_MENU_PARTICLE_EDITOR_SAVE_AS_HPP +#define HEADER_SUPERTUX_SUPERTUX_MENU_PARTICLE_EDITOR_SAVE_AS_HPP + +#include "gui/menu.hpp" + +class ParticleEditorSaveAs final : public Menu +{ +private: + enum MenuIDs { + MNID_SAVE, + MNID_CANCEL + }; + +public: + ParticleEditorSaveAs(std::function callback); + ~ParticleEditorSaveAs(); + + void menu_action(MenuItem& item) override; + +private: + std::string m_filename; + std::function m_callback; + +private: + ParticleEditorSaveAs(const ParticleEditorSaveAs&) = delete; + ParticleEditorSaveAs& operator=(const ParticleEditorSaveAs&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/supertux/resources.cpp b/src/supertux/resources.cpp index a021166f09f..4d033bfb9b7 100644 --- a/src/supertux/resources.cpp +++ b/src/supertux/resources.cpp @@ -35,6 +35,7 @@ FontPtr Resources::fixed_font; FontPtr Resources::normal_font; FontPtr Resources::small_font; FontPtr Resources::big_font; +FontPtr Resources::control_font; SurfacePtr Resources::checkbox; SurfacePtr Resources::checkbox_checked; @@ -59,6 +60,7 @@ Resources::load() normal_font.reset(new BitmapFont(BitmapFont::VARIABLE, "fonts/white.stf")); small_font.reset(new BitmapFont(BitmapFont::VARIABLE, "fonts/white-small.stf", 1)); big_font.reset(new BitmapFont(BitmapFont::VARIABLE, "fonts/white-big.stf", 3)); + control_font.reset(new BitmapFont(BitmapFont::FIXED, "fonts/white.stf")); // TODO: Make a better-looking font for this } else { @@ -72,6 +74,7 @@ Resources::load() normal_font = fixed_font; small_font.reset(new TTFFont(font, 10, 1.25f, 2, 1)); big_font.reset(new TTFFont(font, 22, 1.25f, 2, 1)); + control_font.reset(new TTFFont("fonts/Roboto-Regular.ttf", 15, 1.25f, 0, 0)); } } @@ -113,6 +116,7 @@ Resources::unload() normal_font.reset(); small_font.reset(); big_font.reset(); + control_font.reset(); mouse_cursor.reset(); } diff --git a/src/supertux/resources.hpp b/src/supertux/resources.hpp index eea970cc4d4..672d04e031f 100644 --- a/src/supertux/resources.hpp +++ b/src/supertux/resources.hpp @@ -47,6 +47,9 @@ class Resources final /** Big font for menu titles and headers in text scrolls */ static FontPtr big_font; + /** Font used for control interface elements (particle editor) */ + static FontPtr control_font; + static SurfacePtr checkbox; static SurfacePtr checkbox_checked; static SurfacePtr back; diff --git a/src/supertux/screen_manager.cpp b/src/supertux/screen_manager.cpp index 3407a323270..55776651aa9 100644 --- a/src/supertux/screen_manager.cpp +++ b/src/supertux/screen_manager.cpp @@ -19,6 +19,7 @@ #include "audio/sound_manager.hpp" #include "editor/editor.hpp" +#include "editor/particle_editor.hpp" #include "gui/menu_manager.hpp" #include "object/player.hpp" #include "squirrel/squirrel_virtual_machine.hpp" @@ -308,6 +309,10 @@ ScreenManager::process_events() Editor::current()->event(event); } + if (ParticleEditor::is_active()) { + ParticleEditor::current()->event(event); + } + switch (event.type) { case SDL_QUIT: From f1622ff41e2bcc29d334cf74ef3245121cc41d51 Mon Sep 17 00:00:00 2001 From: Semphris Date: Tue, 13 Oct 2020 21:40:16 -0400 Subject: [PATCH 4/9] Forgot a file. (Particle editor) --- data/particles/custom/default.stcp | 13 +++++++++++++ data/particles/default.stcp | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 data/particles/custom/default.stcp create mode 100644 data/particles/default.stcp diff --git a/data/particles/custom/default.stcp b/data/particles/custom/default.stcp new file mode 100644 index 00000000000..761e0ccae0f --- /dev/null +++ b/data/particles/custom/default.stcp @@ -0,0 +1,13 @@ +(supertux-custom-particle + (z-pos 0) + (main-texture "/images/engine/editor/sparkle.png") + (amount 10) + (delay 0) + (lifetime 1.25) + (birth-time 0) + (death-mode "fade") + (death-time 0.75) + (speed-var-x 256) + (speed-var-y 256) + (cover-screen #f) +) diff --git a/data/particles/default.stcp b/data/particles/default.stcp new file mode 100644 index 00000000000..761e0ccae0f --- /dev/null +++ b/data/particles/default.stcp @@ -0,0 +1,13 @@ +(supertux-custom-particle + (z-pos 0) + (main-texture "/images/engine/editor/sparkle.png") + (amount 10) + (delay 0) + (lifetime 1.25) + (birth-time 0) + (death-mode "fade") + (death-time 0.75) + (speed-var-x 256) + (speed-var-y 256) + (cover-screen #f) +) From 248cb5515f494f0bf955c42d0b46c91b440ba295 Mon Sep 17 00:00:00 2001 From: Semphris Date: Mon, 16 Nov 2020 21:54:01 -0500 Subject: [PATCH 5/9] Custom particles: added feature to use multiple textures in a single custom particle object --- src/editor/particle_editor.cpp | 290 ++++++++++++++++++-------- src/editor/particle_editor.hpp | 4 + src/gui/menu_filesystem.cpp | 5 +- src/object/custom_particle_system.cpp | 149 +++++++++++-- src/object/custom_particle_system.hpp | 8 + src/supertux/game_object.hpp | 2 +- src/video/surface.cpp | 21 +- src/video/surface.hpp | 8 +- 8 files changed, 360 insertions(+), 127 deletions(-) diff --git a/src/editor/particle_editor.cpp b/src/editor/particle_editor.cpp index 3dec6405113..7e4fe81f94a 100644 --- a/src/editor/particle_editor.cpp +++ b/src/editor/particle_editor.cpp @@ -49,8 +49,12 @@ ParticleEditor::ParticleEditor() : m_enabled(true), m_quit_request(false), m_controls(), + m_controls_textures(), + m_texture_rebinds(), m_undo_stack(), m_redo_stack(), + m_in_texture_tab(), + m_texture_current(0), m_saved_version(), m_particles(), m_filename("") @@ -75,18 +79,25 @@ ParticleEditor::reload() m_particles = new CustomParticleSystem(mapping); m_controls.clear(); + m_controls_textures.clear(); + m_texture_rebinds.clear(); + + // ========================================================================== + // MAIN UI + // -------------------------------------------------------------------------- // TODO: Use the addButton() command // Texture button start - auto texture_btn = std::make_unique("Change texture..."); + auto texture_btn = std::make_unique("Change texture... ->"); texture_btn.get()->m_on_change = new std::function([this](){ - const std::vector& filter = {".jpg", ".png", ".surface"}; + /*const std::vector& filter = {".jpg", ".png", ".surface"}; MenuManager::instance().push_menu(std::make_unique( &(m_particles->m_particle_main_texture), filter, "/", [this](std::string new_filename) { m_particles->reinit_textures(); } - )); + ));*/ + m_in_texture_tab = true; }); float tmp_height = 0.f; for (auto& control : m_controls) { @@ -194,7 +205,7 @@ ParticleEditor::reload() offscreen_mode.get()->bind_value(&(m_particles->m_particle_offscreen_mode)); addControl(_("Offscreen mode"), std::move(offscreen_mode)); - // FIXME: add some ParticleEditor::addButton() function so that I don't have to put all that in here + // 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 = new std::function([this](){ m_particles->clear(); }); float height = 0.f; @@ -204,6 +215,160 @@ ParticleEditor::reload() clear_btn.get()->set_rect(Rectf(25.f, height, 325.f, height + 20.f)); m_controls.push_back(std::move(clear_btn)); + // ========================================================================== + // TEXTURE UI + // -------------------------------------------------------------------------- + + auto return_btn = std::make_unique("<- General settings"); + return_btn.get()->m_on_change = new std::function([this](){ + m_in_texture_tab = false; + }); + return_btn.get()->set_rect(Rectf(25.f, 20, 325.f, 40.f)); + m_controls_textures.push_back(std::move(return_btn)); + + auto likeliness_control = std::make_unique(); + 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 = new InterfaceLabel(Rectf(5.f, 50.f, 135.f, 70.f), "Likeliness"); + likeliness_control.get()->m_on_change = new std::function([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)); + }); + m_controls_textures.push_back(std::move(likeliness_control)); + + auto color_r_control = std::make_unique(); + 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 = new InterfaceLabel(Rectf(5.f, 80.f, 140.f, 100.f), "Color (RGBA)"); + color_r_control.get()->m_on_change = new std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + color_r_control.get()->m_validate_float = [](ControlTextboxFloat* t, float f){ return f >= 0.f && f <= 1.f; }; + auto color_r_control_ptr = color_r_control.get(); + m_texture_rebinds.push_back( [this, color_r_control_ptr]{ + color_r_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.red)); + }); + m_controls_textures.push_back(std::move(color_r_control)); + + 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 = new std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + color_g_control.get()->m_validate_float = [](ControlTextboxFloat* t, float f){ return f >= 0.f && f <= 1.f; }; + auto color_g_control_ptr = color_g_control.get(); + m_texture_rebinds.push_back( [this, color_g_control_ptr]{ + color_g_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.green)); + }); + m_controls_textures.push_back(std::move(color_g_control)); + + 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 = new std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + color_b_control.get()->m_validate_float = [](ControlTextboxFloat* t, float f){ return f >= 0.f && f <= 1.f; }; + auto color_b_control_ptr = color_b_control.get(); + m_texture_rebinds.push_back( [this, color_b_control_ptr]{ + color_b_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.blue)); + }); + m_controls_textures.push_back(std::move(color_b_control)); + + 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 = new std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); + color_a_control.get()->m_validate_float = [](ControlTextboxFloat* t, float f){ return f >= 0.f && f <= 1.f; }; + auto color_a_control_ptr = color_a_control.get(); + m_texture_rebinds.push_back( [this, color_a_control_ptr]{ + color_a_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.alpha)); + }); + m_controls_textures.push_back(std::move(color_a_control)); + + auto scale_x_control = std::make_unique(); + 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 = new InterfaceLabel(Rectf(5.f, 110.f, 150.f, 130.f), "Scale (x, y)"); + scale_x_control.get()->m_on_change = new std::function([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)); + }); + m_controls_textures.push_back(std::move(scale_x_control)); + + 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 = new std::function([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)); + }); + m_controls_textures.push_back(std::move(scale_y_control)); + + // Texture button start + auto chg_texture_btn = std::make_unique("Change texture..."); + chg_texture_btn.get()->m_on_change = new std::function([this](){ + const std::vector& filter = {".jpg", ".png", ".surface"}; + MenuManager::instance().push_menu(std::make_unique( + nullptr, + filter, + "/", + [this](std::string new_filename) { + (m_particles->m_textures.begin() + m_texture_current)->texture = Surface::from_file(new_filename); + m_particles->reinit_textures(); + } + )); + }); + chg_texture_btn.get()->set_rect(Rectf(25.f, 370, 325.f, 390)); + m_controls_textures.push_back(std::move(chg_texture_btn)); + // Texture button end + + auto prev_btn = std::make_unique("<"); + prev_btn.get()->m_on_change = new std::function([this](){ + m_texture_current--; + if (m_texture_current < 0) m_texture_current = 0; + for (auto refresh : m_texture_rebinds) + refresh(); + }); + prev_btn.get()->set_rect(Rectf(120.f, 400, 140.f, 420.f)); + m_controls_textures.push_back(std::move(prev_btn)); + + auto del_btn = std::make_unique("-"); + del_btn.get()->m_on_change = new std::function([this](){ + if (m_particles->m_textures.size() < 1) + return; + m_particles->m_textures.erase(m_particles->m_textures.begin() + m_texture_current); + m_texture_current--; + if (m_texture_current < 0) m_texture_current = 0; + for (auto refresh : m_texture_rebinds) + refresh(); + }); + del_btn.get()->set_rect(Rectf(150.f, 400, 170.f, 420.f)); + m_controls_textures.push_back(std::move(del_btn)); + + auto add_btn = std::make_unique("+"); + add_btn.get()->m_on_change = new std::function([this](){ + m_particles->m_textures.push_back(CustomParticleSystem::SpriteProperties()); + m_texture_current = static_cast(m_particles->m_textures.size()) - 1; + for (auto refresh : m_texture_rebinds) + refresh(); + }); + add_btn.get()->set_rect(Rectf(190.f, 400, 210.f, 420.f)); + m_controls_textures.push_back(std::move(add_btn)); + + auto next_btn = std::make_unique(">"); + next_btn.get()->m_on_change = new std::function([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; + for (auto refresh : m_texture_rebinds) + refresh(); + }); + next_btn.get()->set_rect(Rectf(220.f, 400, 240.f, 420.f)); + m_controls_textures.push_back(std::move(next_btn)); + + // ========================================================================== + // THE REST + // -------------------------------------------------------------------------- + m_undo_stack.clear(); m_saved_version = m_particles->get_props(); m_undo_stack.push_back(m_saved_version); @@ -343,7 +508,7 @@ ParticleEditor::save(const std::string& filepath_, bool retry) if (!boost::algorithm::ends_with(filepath, ".stcp")) filepath += ".stcp"; - //FIXME: It tests for directory in supertux/data, but saves into .supertux2. + // FIXME: It tests for directory in supertux/data, but saves into .supertux2. try { { // make sure the level directory exists std::string dirname = FileSystem::dirname(filepath); @@ -436,8 +601,21 @@ ParticleEditor::draw(Compositor& compositor) GradientDirection::HORIZONTAL, Rectf(0.f, 0.f, 355.f, float(context.get_height()))); - for(const auto& control : m_controls) { - control->draw(context); + if (m_in_texture_tab) + { + context.color().draw_surface_scaled((m_particles->m_textures.begin() + m_texture_current)->texture, + Rect(75, 150, 275, 350), LAYER_GUI); + for(const auto& control : m_controls_textures) + { + control->draw(context); + } + } + else + { + for(const auto& control : m_controls) + { + control->draw(context); + } } MouseCursor::current()->draw(context); @@ -453,8 +631,14 @@ ParticleEditor::update(float dt_sec, const Controller& controller) quit_editor(); } - for(const auto& control : m_controls) { - control->update(dt_sec, controller); + if (m_in_texture_tab) { + for(const auto& control : m_controls_textures) { + control->update(dt_sec, controller); + } + } else { + for(const auto& control : m_controls) { + control->update(dt_sec, controller); + } } // Uncomment to make the particles stop updating on pause @@ -484,12 +668,6 @@ ParticleEditor::quit_editor() auto quit = [] () { - //Quit particle editor - /*m_world = nullptr; - m_levelfile = ""; - m_levelloaded = false; - m_enabled = false; - Tile::draw_editor_images = false;*/ ScreenManager::current()->pop_screen(); if (Editor::current()) { Editor::current()->m_reactivate_request = true; @@ -568,9 +746,14 @@ ParticleEditor::event(const SDL_Event& ev) { if (!m_enabled) return; - for(const auto& control : m_controls) { - if (control->event(ev)) - break; + if (m_in_texture_tab) { + for(const auto& control : m_controls_textures) + if (control->event(ev)) + break; + } else { + for(const auto& control : m_controls) + if (control->event(ev)) + break; } if (ev.type == SDL_KEYDOWN && @@ -596,77 +779,6 @@ ParticleEditor::event(const SDL_Event& ev) ev.key.keysym.mod & KMOD_CTRL) { MenuManager::instance().set_menu(MenuStorage::PARTICLE_EDITOR_OPEN); } - -/* - if (!m_enabled) return; - - try - { - if (ev.type == SDL_KEYDOWN && - ev.key.keysym.sym == SDLK_t && - ev.key.keysym.mod & KMOD_CTRL) { - test_level(boost::none); - } - - if (ev.type == SDL_KEYDOWN && - ev.key.keysym.sym == SDLK_s && - ev.key.keysym.mod & KMOD_CTRL) { - save_level(); - } - - if (ev.type == SDL_KEYDOWN && - ev.key.keysym.sym == SDLK_z && - ev.key.keysym.mod & KMOD_CTRL) { - undo(); - } - - if (ev.type == SDL_KEYDOWN && - ev.key.keysym.sym == SDLK_y && - ev.key.keysym.mod & KMOD_CTRL) { - redo(); - } - - if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F6) { - Compositor::s_render_lighting = !Compositor::s_render_lighting; - return; - } - - m_ignore_sector_change = false; - - BIND_SECTOR(*m_sector); - - for(const auto& widget : m_controls) { - if (widget->event(ev)) - break; - } - - // unreliable heuristic to snapshot the current state for future undo - if (((ev.type == SDL_KEYUP && ev.key.repeat == 0 && - ev.key.keysym.sym != SDLK_LSHIFT && - ev.key.keysym.sym != SDLK_RSHIFT && - ev.key.keysym.sym != SDLK_LCTRL && - ev.key.keysym.sym != SDLK_RCTRL) || - ev.type == SDL_MOUSEBUTTONUP)) - { - if (!m_ignore_sector_change) { - if (m_level) { - m_undo_manager->try_snapshot(*m_level); - } - } - } - - // Scroll with mouse wheel, if the mouse is not over the toolbox. - // The toolbox does scrolling independently from the main area. - if (ev.type == SDL_MOUSEWHEEL && !m_toolbox_widget->has_mouse_focus() && !m_layers_widget->has_mouse_focus()) { - float scroll_x = static_cast(ev.wheel.x * -32); - float scroll_y = static_cast(ev.wheel.y * -32); - scroll({scroll_x, scroll_y}); - } - } - catch(const std::exception& err) - { - log_warning << "error while processing ParticleEditor::event(): " << err.what() << std::endl; - }*/ } void diff --git a/src/editor/particle_editor.hpp b/src/editor/particle_editor.hpp index 34092ceaa34..2301805627c 100644 --- a/src/editor/particle_editor.hpp +++ b/src/editor/particle_editor.hpp @@ -102,8 +102,12 @@ class ParticleEditor final : public Screen, private: std::vector> m_controls; + std::vector> m_controls_textures; + std::vector> m_texture_rebinds; std::vector> m_undo_stack; std::vector> m_redo_stack; + bool m_in_texture_tab; + int m_texture_current; std::shared_ptr m_saved_version; diff --git a/src/gui/menu_filesystem.cpp b/src/gui/menu_filesystem.cpp index 241a0618f70..062d9153dad 100644 --- a/src/gui/menu_filesystem.cpp +++ b/src/gui/menu_filesystem.cpp @@ -146,10 +146,11 @@ FileSystemMenu::menu_action(MenuItem& item) new_filename = FileSystem::relpath(new_filename, m_basedir); } - *m_filename = new_filename; + if (m_filename) + *m_filename = new_filename; if (m_callback) - m_callback(*m_filename); + m_callback(new_filename); MenuManager::instance().pop_menu(); } else { diff --git a/src/object/custom_particle_system.cpp b/src/object/custom_particle_system.cpp index 21fc9c52e63..9d0cef59f12 100644 --- a/src/object/custom_particle_system.cpp +++ b/src/object/custom_particle_system.cpp @@ -33,6 +33,7 @@ #include "supertux/sector.hpp" #include "supertux/tile.hpp" #include "util/reader.hpp" +#include "util/reader_document.hpp" #include "util/reader_mapping.hpp" #include "video/drawing_context.hpp" #include "video/surface.hpp" @@ -126,6 +127,78 @@ CustomParticleSystem::CustomParticleSystem(const ReaderMapping& reader) : { reader.get("main-texture", m_particle_main_texture, "/images/engine/editor/sparkle.png"); + // FIXME: Is there a cleaner way to get a list of textures? + auto iter = reader.get_iter(); + while (iter.next()) + { + if (iter.get_key() == "texture") + { + ReaderMapping mapping = iter.as_mapping(); + + std::string tex; + if (!mapping.get("surface", tex)) + { + log_warning << "Texture without surface data ('surface') in " << + mapping.get_doc().get_filename() << ", skipping" << std::endl; + continue; + } + + float color_r, color_g, color_b, color_a; + if (!mapping.get("color_r", color_r)) + { + log_warning << "Texture without red color field ('color_r') in " << + mapping.get_doc().get_filename() << ", skipping" << std::endl; + continue; + } + if (!mapping.get("color_g", color_g)) + { + log_warning << "Texture without green color field ('color_g') in " << + mapping.get_doc().get_filename() << ", skipping" << std::endl; + continue; + } + if (!mapping.get("color_b", color_b)) + { + log_warning << "Texture without blue color field ('color_b') in " << + mapping.get_doc().get_filename() << ", skipping" << std::endl; + continue; + } + if (!mapping.get("color_a", color_a)) + { + log_warning << "Texture without alpha channel field ('color_a') in " << + mapping.get_doc().get_filename() << ", skipping" << std::endl; + continue; + } + + float likeliness; + if (!mapping.get("likeliness", likeliness)) + { + log_warning << "Texture without likeliness field in " << + mapping.get_doc().get_filename() << ", skipping" << std::endl; + continue; + } + + float scale_x, scale_y; + if (!mapping.get("scale_x", scale_x)) + { + log_warning << "Texture without horizontal scale ('scale_x') field in " << + mapping.get_doc().get_filename() << ", skipping" << std::endl; + continue; + } + if (!mapping.get("scale_y", scale_y)) + { + log_warning << "Texture without vertical scale ('scale_y') field in " << + mapping.get_doc().get_filename() << ", skipping" << std::endl; + continue; + } + + auto props = SpriteProperties(Surface::from_file(tex)); + props.likeliness = likeliness; + props.color = Color(color_r, color_g, color_b, color_a); + props.scale = Vector(scale_x, scale_y); + m_textures.push_back(props); + } + } + reader.get("amount", m_max_amount, 25); reader.get("delay", m_delay, 0.1f); reader.get("lifetime", m_particle_lifetime, 5.f); @@ -297,22 +370,40 @@ CustomParticleSystem::~CustomParticleSystem() void CustomParticleSystem::reinit_textures() { - m_textures.clear(); - // TODO: Multiple textures for a single particle system object? - - //m_textures.push_back(SpriteProperties(Surface::from_file("images/engine/editor/sparkle.png"))); - //for (float r = 0.f; r < 1.f; r += 0.5f) { - // for (float g = 0.f; g < 1.f; g += 0.5f) { - // for (float b = 0.f; b < 1.f; b += 0.5f) { - auto props = SpriteProperties(Surface::from_file(m_particle_main_texture)); - props.likeliness = 1.f; - props.color = Color(1.f, 1.f, 1.f, 1.f); - props.scale = Vector(1.f, 1.f); - m_textures.push_back(props); - // } - // } - //} - texture_sum_odds = 1.f; + if (!m_textures.size()) + { + auto props = SpriteProperties(Surface::from_file(m_particle_main_texture)); + props.likeliness = 1.f; + props.color = Color(1.f, 1.f, 1.f, 1.f); + props.scale = Vector(1.f, 1.f); + m_textures.push_back(props); + } + + texture_sum_odds = 0.f; + for (auto texture : m_textures) + { + texture_sum_odds += texture.likeliness; + } +} + +void +CustomParticleSystem::save(Writer& writer) +{ + for (auto tex : m_textures) + { + writer.start_list("texture"); + writer.write("surface", tex.texture->get_filename()); + writer.write("color_r", tex.color.red); + writer.write("color_g", tex.color.green); + writer.write("color_b", tex.color.blue); + writer.write("color_a", tex.color.alpha); + writer.write("likeliness", tex.likeliness); + writer.write("scale_x", tex.scale.x); + writer.write("scale_y", tex.scale.y); + writer.end_list("texture"); + } + + GameObject::save(writer); } ObjectSettings @@ -321,6 +412,7 @@ CustomParticleSystem::get_settings() ObjectSettings result = ParticleSystem::get_settings(); result.add_surface(_("Texture"), &m_particle_main_texture, "main-texture"); + result.add_int(_("Amount"), &m_max_amount, "amount", 25); result.add_float(_("Delay"), &m_delay, "delay", 0.1f); result.add_float(_("Lifetime"), &m_particle_lifetime, "lifetime", 5.f); @@ -701,16 +793,25 @@ CustomParticleSystem::draw(DrawingContext& context) if (it == batches.end()) { const auto& batch_it = batches.emplace(&(particle->props), SurfaceBatch(particle->props.texture, particle->props.color)); - batch_it.first->second.draw(Rectf(particle->pos, + batch_it.first->second.draw(Rectf(Vector( + particle->pos.x - particle->scale + * static_cast( + particle->props.texture->get_width() + ) * particle->props.scale.x / 2, + particle->pos.y - particle->scale + * static_cast( + particle->props.texture->get_height() + ) * particle->props.scale.y / 2 + ), Vector( particle->pos.x + particle->scale * static_cast( particle->props.texture->get_width() - ) * particle->props.scale.x, + ) * particle->props.scale.x / 2, particle->pos.y + particle->scale * static_cast( particle->props.texture->get_height() - ) * particle->props.scale.y + ) * particle->props.scale.y / 2 ) ), particle->angle); } else { @@ -754,18 +855,20 @@ CustomParticleSystem::collision(Particle* object, const Vector& movement) float x1, x2; float y1, y2; - x1 = object->pos.x; + x1 = object->pos.x - particle->props.scale.x * static_cast(particle->props.texture->get_width()) / 2; x2 = x1 + particle->props.scale.x * static_cast(particle->props.texture->get_width()) + movement.x; if (x2 < x1) { + float temp_x = x1; x1 = x2; - x2 = object->pos.x; + x2 = temp_x; } - y1 = object->pos.y; + y1 = object->pos.y - particle->props.scale.y * static_cast(particle->props.texture->get_height()) / 2; y2 = y1 + particle->props.scale.y * static_cast(particle->props.texture->get_height()) + movement.y; if (y2 < y1) { + float temp_y = y1; y1 = y2; - y2 = object->pos.y; + y2 = temp_y; } bool water = false; diff --git a/src/object/custom_particle_system.hpp b/src/object/custom_particle_system.hpp index 33f5095f4f4..46ef59050bf 100644 --- a/src/object/custom_particle_system.hpp +++ b/src/object/custom_particle_system.hpp @@ -42,6 +42,7 @@ class CustomParticleSystem : virtual std::string get_class() const override { return "particles-custom"; } virtual std::string get_display_name() const override { return _("Custom Particles"); } + virtual void save(Writer& writer) override; virtual ObjectSettings get_settings() override; virtual const std::string get_icon_path() const override { @@ -243,6 +244,7 @@ class CustomParticleSystem : class ParticleProps final { public: + std::vector m_textures; std::string m_particle_main_texture; int m_max_amount; float m_delay; @@ -277,6 +279,7 @@ class CustomParticleSystem : bool m_cover_screen; ParticleProps() : + m_textures(), m_particle_main_texture(), m_max_amount(25), m_delay(0.1f), @@ -317,6 +320,8 @@ class CustomParticleSystem : { std::shared_ptr props = std::make_shared(); + for (auto texture : m_textures) + props->m_textures.push_back(texture); props->m_particle_main_texture = m_particle_main_texture; props->m_max_amount = m_max_amount; props->m_delay = m_delay; @@ -355,6 +360,9 @@ class CustomParticleSystem : void set_props(std::shared_ptr props) { + m_textures.clear(); + for (auto texture : props->m_textures) + m_textures.push_back(texture); m_particle_main_texture = props->m_particle_main_texture; m_max_amount = props->m_max_amount; m_delay = props->m_delay; diff --git a/src/supertux/game_object.hpp b/src/supertux/game_object.hpp index 21ded83aae8..3ff3e0f1451 100644 --- a/src/supertux/game_object.hpp +++ b/src/supertux/game_object.hpp @@ -72,7 +72,7 @@ class GameObject virtual void draw(DrawingContext& context) = 0; /** This function saves the object. Editor will use that. */ - void save(Writer& writer); + virtual void save(Writer& writer); virtual std::string get_class() const { return "game-object"; } virtual std::string get_display_name() const { return _("Unknown object"); } diff --git a/src/video/surface.cpp b/src/video/surface.cpp index 0752aced5ab..ec536599e21 100644 --- a/src/video/surface.cpp +++ b/src/video/surface.cpp @@ -26,7 +26,7 @@ #include "video/video_system.hpp" SurfacePtr -Surface::from_reader(const ReaderMapping& mapping, const boost::optional& rect) +Surface::from_reader(const ReaderMapping& mapping, const boost::optional& rect, const std::string& filename) { TexturePtr diffuse_texture; boost::optional diffuse_texture_mapping; @@ -50,7 +50,8 @@ Surface::from_reader(const ReaderMapping& mapping, const boost::optional& flip ^= flip_v[1] ? VERTICAL_FLIP : NO_FLIP; } - return SurfacePtr(new Surface(diffuse_texture, displacement_texture, flip)); + auto surface = new Surface(diffuse_texture, displacement_texture, flip, filename); + return SurfacePtr(surface); } SurfacePtr @@ -68,7 +69,7 @@ Surface::from_file(const std::string& filename, const boost::optional& rec } else { - return Surface::from_reader(object.get_mapping(), rect); + return Surface::from_reader(object.get_mapping(), rect, filename); } } else @@ -76,34 +77,36 @@ Surface::from_file(const std::string& filename, const boost::optional& rec if (rect) { TexturePtr texture = TextureManager::current()->get(filename, *rect); - return SurfacePtr(new Surface(texture, TexturePtr(), NO_FLIP)); + return SurfacePtr(new Surface(texture, TexturePtr(), NO_FLIP, filename)); } else { TexturePtr texture = TextureManager::current()->get(filename); - return SurfacePtr(new Surface(texture, TexturePtr(), NO_FLIP)); + return SurfacePtr(new Surface(texture, TexturePtr(), NO_FLIP, filename)); } } } Surface::Surface(const TexturePtr& diffuse_texture, const TexturePtr& displacement_texture, - Flip flip) : + Flip flip, const std::string& filename) : m_diffuse_texture(diffuse_texture), m_displacement_texture(displacement_texture), m_region(0, 0, m_diffuse_texture->get_image_width(), m_diffuse_texture->get_image_height()), - m_flip(flip) + m_flip(flip), + m_source_filename(filename) { } Surface::Surface(const TexturePtr& diffuse_texture, const TexturePtr& displacement_texture, const Rect& region, - Flip flip) : + Flip flip, const std::string& filename) : m_diffuse_texture(diffuse_texture), m_displacement_texture(displacement_texture), m_region(region), - m_flip(flip) + m_flip(flip), + m_source_filename(filename) { } diff --git a/src/video/surface.hpp b/src/video/surface.hpp index b1377532c42..609514564df 100644 --- a/src/video/surface.hpp +++ b/src/video/surface.hpp @@ -37,11 +37,11 @@ class Surface final public: static SurfacePtr from_texture(const TexturePtr& texture); static SurfacePtr from_file(const std::string& filename, const boost::optional& rect = boost::none); - static SurfacePtr from_reader(const ReaderMapping& mapping, const boost::optional& rect = boost::none); + static SurfacePtr from_reader(const ReaderMapping& mapping, const boost::optional& rect = boost::none, const std::string& filename = ""); private: - Surface(const TexturePtr& diffuse_texture, const TexturePtr& displacement_texture, Flip flip); - Surface(const TexturePtr& diffuse_texture, const TexturePtr& displacement_texture, const Rect& region, Flip flip); + Surface(const TexturePtr& diffuse_texture, const TexturePtr& displacement_texture, Flip flip, const std::string& filename = ""); + Surface(const TexturePtr& diffuse_texture, const TexturePtr& displacement_texture, const Rect& region, Flip flip, const std::string& filename = ""); public: ~Surface(); @@ -55,12 +55,14 @@ class Surface final int get_width() const; int get_height() const; Flip get_flip() const { return m_flip; } + std::string get_filename() const { return m_source_filename; } private: const TexturePtr m_diffuse_texture; const TexturePtr m_displacement_texture; const Rect m_region; const Flip m_flip; + const std::string m_source_filename; private: Surface& operator=(const Surface&) = delete; From 7b1c5dce393d84f8941d8a6903acdc4a233bdf53 Mon Sep 17 00:00:00 2001 From: Semphriss <66701383+Semphriss@users.noreply.github.com> Date: Tue, 17 Nov 2020 14:35:13 +0000 Subject: [PATCH 6/9] Custom particles fix: forgot a comma --- src/supertux/menu/menu_storage.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/supertux/menu/menu_storage.hpp b/src/supertux/menu/menu_storage.hpp index bc703ef8c4c..8e6a996c597 100644 --- a/src/supertux/menu/menu_storage.hpp +++ b/src/supertux/menu/menu_storage.hpp @@ -66,7 +66,7 @@ class MenuStorage final EDITOR_LEVELSET_MENU, PARTICLE_EDITOR_MENU, PARTICLE_EDITOR_SAVE_AS, - PARTICLE_EDITOR_OPEN + PARTICLE_EDITOR_OPEN, INTEGRATIONS_MENU }; From 5b9c8c8c246d308f8c05e600fdcb0a6905e50a1e Mon Sep 17 00:00:00 2001 From: Semphris Date: Tue, 17 Nov 2020 20:47:38 -0500 Subject: [PATCH 7/9] Finalized custom particles Added: Opening the particle editor through a custom-particle-from-file object now opens its corresponding file Added: It is now possible to add multiple textures to a single custom particle object through the particle editor Added: Saving particles now support multiple textures per object Added: It is now possible to make particles bounce on solid surfaces Done: Code cleanup and const correctness :) --- src/editor/editor.cpp | 3 + src/editor/editor.hpp | 1 + src/editor/particle_editor.cpp | 127 ++++++++------- src/editor/particle_editor.hpp | 24 +-- src/editor/particle_settings_widget.cpp | 150 ------------------ src/editor/particle_settings_widget.hpp | 80 ---------- src/interface/control.hpp | 4 +- src/interface/control_button.cpp | 45 +++--- src/interface/control_checkbox.cpp | 40 +++-- src/interface/control_checkbox.hpp | 2 +- src/interface/control_enum.cpp | 2 +- src/interface/control_enum.hpp | 42 ++--- src/interface/control_textbox.cpp | 16 +- src/interface/control_textbox.hpp | 16 +- src/interface/control_textbox_float.hpp | 2 +- src/interface/control_textbox_int.hpp | 2 +- src/interface/label.cpp | 4 +- src/interface/label.hpp | 8 +- src/object/custom_particle_system.cpp | 120 ++++++++++++-- src/object/custom_particle_system.hpp | 9 +- src/object/custom_particle_system_file.cpp | 6 +- src/supertux/menu/particle_editor_save_as.cpp | 2 +- src/supertux/menu/particle_editor_save_as.hpp | 2 +- src/video/surface_batch.hpp | 2 +- 24 files changed, 303 insertions(+), 406 deletions(-) delete mode 100644 src/editor/particle_settings_widget.cpp delete mode 100644 src/editor/particle_settings_widget.hpp diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 247739060b0..1ea3a088c1c 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -88,6 +88,7 @@ Editor::Editor() : m_particle_editor_request(false), m_test_pos(), m_savegame(), + m_particle_editor_filename(), m_sector(), m_levelloaded(false), m_leveltested(false), @@ -186,6 +187,8 @@ Editor::update(float dt_sec, const Controller& controller) if (m_particle_editor_request) { m_particle_editor_request = false; std::unique_ptr screen(new ParticleEditor()); + if (m_particle_editor_filename) + static_cast(screen.get())->open("particles/" + *m_particle_editor_filename); ScreenManager::current()->push_screen(move(screen)); return; } diff --git a/src/editor/editor.hpp b/src/editor/editor.hpp index 127c32b3ff7..b9bd8775895 100644 --- a/src/editor/editor.hpp +++ b/src/editor/editor.hpp @@ -159,6 +159,7 @@ class Editor final : public Screen, boost::optional> m_test_pos; std::unique_ptr m_savegame; + std::string* m_particle_editor_filename; private: Sector* m_sector; diff --git a/src/editor/particle_editor.cpp b/src/editor/particle_editor.cpp index 7e4fe81f94a..bc94107c3b3 100644 --- a/src/editor/particle_editor.cpp +++ b/src/editor/particle_editor.cpp @@ -16,6 +16,9 @@ #include "editor/particle_editor.hpp" +#include +#include + #include "control/input_manager.hpp" #include "editor/editor.hpp" #include "gui/dialog.hpp" @@ -35,16 +38,25 @@ #include "util/reader_mapping.hpp" #include "video/compositor.hpp" -#include -#include bool ParticleEditor::is_active() { - auto* self = ParticleEditor::current(); - return self && true; + return ParticleEditor::current() != nullptr; } +bool (*ParticleEditor::m_clamp_0_1)(ControlTextboxFloat*, float) = [](ControlTextboxFloat* c, float f){ + if (f < 0.f) { + c->set_value(0.f); + return false; + } else if (f > 1.f) { + c->set_value(1.f); + return false; + } else { + return true; + } +}; + ParticleEditor::ParticleEditor() : m_enabled(true), m_quit_request(false), @@ -65,10 +77,23 @@ ParticleEditor::ParticleEditor() : void ParticleEditor::reload() { - // TODO: Use a std::unique_ptr here - if (m_particles) - delete m_particles; + reload_particles(); + reset_main_ui(); + reset_texture_ui(); + m_redo_stack.clear(); + m_undo_stack.clear(); + m_saved_version = m_particles->get_props(); + m_undo_stack.push_back(m_saved_version); +} + +ParticleEditor::~ParticleEditor() +{ +} + +void +ParticleEditor::reload_particles() +{ auto doc = ReaderDocument::from_file((m_filename == "") ? "/particles/default.stcp" : m_filename); auto root = doc.get_root(); auto mapping = root.get_mapping(); @@ -76,31 +101,22 @@ ParticleEditor::reload() if (root.get_name() != "supertux-custom-particle") throw std::runtime_error("file is not a supertux-custom-particle file."); - m_particles = new CustomParticleSystem(mapping); + m_particles.reset(new CustomParticleSystem(mapping)); +} +void +ParticleEditor::reset_main_ui() +{ m_controls.clear(); - m_controls_textures.clear(); - m_texture_rebinds.clear(); - - // ========================================================================== - // MAIN UI - // -------------------------------------------------------------------------- // TODO: Use the addButton() command // Texture button start auto texture_btn = std::make_unique("Change texture... ->"); texture_btn.get()->m_on_change = new std::function([this](){ - /*const std::vector& filter = {".jpg", ".png", ".surface"}; - MenuManager::instance().push_menu(std::make_unique( - &(m_particles->m_particle_main_texture), - filter, - "/", - [this](std::string new_filename) { m_particles->reinit_textures(); } - ));*/ m_in_texture_tab = true; }); float tmp_height = 0.f; - for (auto& control : m_controls) { + for (const auto& control : m_controls) { tmp_height = std::max(tmp_height, control->get_rect().get_bottom() + 5.f); } texture_btn.get()->set_rect(Rectf(25.f, tmp_height, 325.f, tmp_height + 20.f)); @@ -112,12 +128,14 @@ ParticleEditor::reload() if (i < 0) { ctrl->set_value(0); return false; - } else if (i > 500) { + } + + if (i > 500) { ctrl->set_value(500); return false; - } else { - return true; } + + return true; } ); addTextboxFloat(_("Delay"), &(m_particles->m_delay)); @@ -209,21 +227,24 @@ ParticleEditor::reload() auto clear_btn = std::make_unique("Clear"); clear_btn.get()->m_on_change = new std::function([this](){ m_particles->clear(); }); float height = 0.f; - for (auto& control : m_controls) { + for (const auto& control : m_controls) { height = std::max(height, control->get_rect().get_bottom() + 5.f); } clear_btn.get()->set_rect(Rectf(25.f, height, 325.f, height + 20.f)); m_controls.push_back(std::move(clear_btn)); +} - // ========================================================================== - // TEXTURE UI - // -------------------------------------------------------------------------- +void +ParticleEditor::reset_texture_ui() +{ + m_controls_textures.clear(); + m_texture_rebinds.clear(); auto return_btn = std::make_unique("<- General settings"); return_btn.get()->m_on_change = new std::function([this](){ m_in_texture_tab = false; }); - return_btn.get()->set_rect(Rectf(25.f, 20, 325.f, 40.f)); + return_btn.get()->set_rect(Rectf(25.f, 0.f, 325.f, 20.f)); m_controls_textures.push_back(std::move(return_btn)); auto likeliness_control = std::make_unique(); @@ -242,7 +263,7 @@ ParticleEditor::reload() color_r_control.get()->set_rect(Rectf(150.f, 80.f, 192.f, 100.f)); color_r_control.get()->m_label = new InterfaceLabel(Rectf(5.f, 80.f, 140.f, 100.f), "Color (RGBA)"); color_r_control.get()->m_on_change = new std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); - color_r_control.get()->m_validate_float = [](ControlTextboxFloat* t, float f){ return f >= 0.f && f <= 1.f; }; + 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]{ color_r_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.red)); @@ -253,7 +274,7 @@ ParticleEditor::reload() 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 = new std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); - color_g_control.get()->m_validate_float = [](ControlTextboxFloat* t, float f){ return f >= 0.f && f <= 1.f; }; + 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]{ color_g_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.green)); @@ -264,7 +285,7 @@ ParticleEditor::reload() 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 = new std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); - color_b_control.get()->m_validate_float = [](ControlTextboxFloat* t, float f){ return f >= 0.f && f <= 1.f; }; + 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]{ color_b_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.blue)); @@ -275,7 +296,7 @@ ParticleEditor::reload() 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 = new std::function([this](){ m_particles->reinit_textures(); this->push_version(); }); - color_a_control.get()->m_validate_float = [](ControlTextboxFloat* t, float f){ return f >= 0.f && f <= 1.f; }; + 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]{ color_a_control_ptr->bind_value(&((m_particles->m_textures.begin() + m_texture_current)->color.alpha)); @@ -325,7 +346,7 @@ ParticleEditor::reload() prev_btn.get()->m_on_change = new std::function([this](){ m_texture_current--; if (m_texture_current < 0) m_texture_current = 0; - for (auto refresh : m_texture_rebinds) + for (const auto& refresh : m_texture_rebinds) refresh(); }); prev_btn.get()->set_rect(Rectf(120.f, 400, 140.f, 420.f)); @@ -338,7 +359,7 @@ ParticleEditor::reload() m_particles->m_textures.erase(m_particles->m_textures.begin() + m_texture_current); m_texture_current--; if (m_texture_current < 0) m_texture_current = 0; - for (auto refresh : m_texture_rebinds) + for (const auto& refresh : m_texture_rebinds) refresh(); }); del_btn.get()->set_rect(Rectf(150.f, 400, 170.f, 420.f)); @@ -348,7 +369,7 @@ ParticleEditor::reload() add_btn.get()->m_on_change = new std::function([this](){ m_particles->m_textures.push_back(CustomParticleSystem::SpriteProperties()); m_texture_current = static_cast(m_particles->m_textures.size()) - 1; - for (auto refresh : m_texture_rebinds) + for (const auto& refresh : m_texture_rebinds) refresh(); }); add_btn.get()->set_rect(Rectf(190.f, 400, 210.f, 420.f)); @@ -359,23 +380,11 @@ ParticleEditor::reload() 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; - for (auto refresh : m_texture_rebinds) + for (const auto& refresh : m_texture_rebinds) refresh(); }); next_btn.get()->set_rect(Rectf(220.f, 400, 240.f, 420.f)); m_controls_textures.push_back(std::move(next_btn)); - - // ========================================================================== - // THE REST - // -------------------------------------------------------------------------- - - m_undo_stack.clear(); - m_saved_version = m_particles->get_props(); - m_undo_stack.push_back(m_saved_version); -} - -ParticleEditor::~ParticleEditor() -{ } void @@ -403,7 +412,7 @@ ParticleEditor::addTextboxFloatWithImprecision(std::string name, float* bind, // Can't use addControl() because this is a special case float height = 0.f; - for (auto& control : m_controls) { + for (const auto& control : m_controls) { height = std::max(height, control->get_rect().get_bottom() + 5.f); } @@ -482,7 +491,7 @@ void ParticleEditor::addControl(std::string name, std::unique_ptr new_control) { float height = 0.f; - for (auto& control : m_controls) { + for (const auto& control : m_controls) { height = std::max(height, control->get_rect().get_bottom() + 5.f); } @@ -509,6 +518,7 @@ ParticleEditor::save(const std::string& filepath_, bool retry) filepath += ".stcp"; // FIXME: It tests for directory in supertux/data, but saves into .supertux2. + // Note: I remember writing this but I have no clue what I meant. ~Semphris try { { // make sure the level directory exists std::string dirname = FileSystem::dirname(filepath); @@ -592,9 +602,6 @@ ParticleEditor::draw(Compositor& compositor) m_particles->draw(context); - /*context.color().draw_filled_rect(Rectf(0.f, 0.f, 255.f, context.get_height()), - Color(), - LAYER_GUI - 1);*/ context.color().draw_gradient(Color(0.05f, 0.1f, 0.1f, 1.f), Color(0.1f, 0.15f, 0.15f, 1.f), LAYER_GUI - 1, @@ -605,6 +612,12 @@ ParticleEditor::draw(Compositor& compositor) { context.color().draw_surface_scaled((m_particles->m_textures.begin() + m_texture_current)->texture, Rect(75, 150, 275, 350), LAYER_GUI); + context.color().draw_text(Resources::control_font, + std::to_string(m_texture_current + 1) + "/" + + std::to_string(m_particles->m_textures.size()), + Vector(175, 30), + FontAlignment::ALIGN_CENTER, + LAYER_GUI); for(const auto& control : m_controls_textures) { control->draw(context); @@ -796,7 +809,7 @@ ParticleEditor::undo() m_redo_stack.push_back(m_undo_stack.back()); m_undo_stack.pop_back(); - m_particles->set_props(m_undo_stack.back()); + m_particles->set_props(m_undo_stack.back().get()); } void @@ -806,7 +819,7 @@ ParticleEditor::redo() return; m_undo_stack.push_back(m_redo_stack.back()); - m_particles->set_props(m_redo_stack.back()); + m_particles->set_props(m_redo_stack.back().get()); m_redo_stack.pop_back(); } diff --git a/src/editor/particle_editor.hpp b/src/editor/particle_editor.hpp index 2301805627c..5b5aa013a82 100644 --- a/src/editor/particle_editor.hpp +++ b/src/editor/particle_editor.hpp @@ -1,5 +1,5 @@ // SuperTux -// Copyright (C) 2015 Hume2 +// Copyright (C) 2020 A. Semphris // // 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 @@ -17,12 +17,12 @@ #ifndef HEADER_SUPERTUX_EDITOR_PARTICLE_EDITOR_HPP #define HEADER_SUPERTUX_EDITOR_PARTICLE_EDITOR_HPP -#include #include #include #include -#include "editor/particle_settings_widget.hpp" +#include + #include "interface/control.hpp" #include "interface/control_button.hpp" #include "interface/control_enum.hpp" @@ -43,10 +43,14 @@ class ParticleEditor final : public Screen, public: static bool is_active(); +private: + static bool (*m_clamp_0_1)(ControlTextboxFloat*, float); + public: ParticleEditor(); ~ParticleEditor(); +public: virtual void draw(Compositor&) override; virtual void update(float dt_sec, const Controller& controller) override; @@ -74,11 +78,11 @@ class ParticleEditor final : public Screen, */ void reload(); -public: - bool m_enabled; - bool m_quit_request; - private: + void reload_particles(); + void reset_main_ui(); + void reset_texture_ui(); + void addTextboxFloat(std::string name, float* bind, bool (*float_validator)(ControlTextboxFloat*, float) = nullptr); @@ -100,7 +104,9 @@ class ParticleEditor final : public Screen, void undo(); void redo(); -private: +public: + bool m_enabled; + bool m_quit_request; std::vector> m_controls; std::vector> m_controls_textures; std::vector> m_texture_rebinds; @@ -111,7 +117,7 @@ class ParticleEditor final : public Screen, std::shared_ptr m_saved_version; - CustomParticleSystem* m_particles; + std::unique_ptr m_particles; std::string m_filename; private: diff --git a/src/editor/particle_settings_widget.cpp b/src/editor/particle_settings_widget.cpp deleted file mode 100644 index 77b0db031d9..00000000000 --- a/src/editor/particle_settings_widget.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// SuperTux -// Copyright (C) 2020 A. Semphris -// -// 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 . - -#if 0 -#include "editor/particle_settings_widget.hpp" - -#include - -#include "editor/editor.hpp" -#include "video/drawing_context.hpp" -#include "video/renderer.hpp" -#include "video/video_system.hpp" -#include "video/viewport.hpp" - -ParticleSettingsWidget::ParticleSettingsWidget() : - m_scrolling(), - m_hovering(), - m_total_region(), - m_covered_region(), - m_progress(), - m_rect(), - m_scaled_rect(), - is_horizontal(), - last_mouse_pos(), - zoom_factor() -{ - m_covered_region = VideoSystem::current()->get_viewport().get_rect().get_height(); - m_total_region = 2000; -} - -void -ParticleSettingsWidget::draw(DrawingContext& context) -{ - m_rect = Rect(10, 0, 20, context.get_height()); - - context.color().draw_filled_rect(m_rect, Color(0.5f, 0.5f, 0.5f, 1.f), 8, LAYER_GUI); - context.color().draw_filled_rect(get_bar_rect(), - Color(1.f, 1.f, 1.f, (m_hovering || m_scrolling) ? 1.f : 0.5f), - 8, - LAYER_GUI); -/* - context.color().draw_filled_rect(Rectf(Vector(0, 0), Vector(SIZE, SIZE)), - Color(0.9f, 0.9f, 1.0f, 0.6f), - MIDDLE, LAYER_GUI-10); - context.color().draw_filled_rect(Rectf(Vector(40, 40), Vector(56, 56)), - Color(0.9f, 0.9f, 1.0f, 0.6f), - 8, LAYER_GUI-20); - if (can_scroll()) { - draw_arrow(context, m_mouse_pos); - } - - draw_arrow(context, Vector(TOPLEFT, MIDDLE)); - draw_arrow(context, Vector(BOTTOMRIGHT, MIDDLE)); - draw_arrow(context, Vector(MIDDLE, TOPLEFT)); - draw_arrow(context, Vector(MIDDLE, BOTTOMRIGHT)); -*/ -} - -void -ParticleSettingsWidget::update(float dt_sec) -{ - -} - -bool -ParticleSettingsWidget::on_mouse_button_up(const SDL_MouseButtonEvent& button) -{ - m_scrolling = false; - return false; -} - -bool -ParticleSettingsWidget::on_mouse_button_down(const SDL_MouseButtonEvent& button) -{ - if (button.button == SDL_BUTTON_LEFT) { - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); - if (get_bar_rect().contains(int(mouse_pos.x), int(mouse_pos.y))) { - m_scrolling = true; - return true; - } else { - return false; - } - } else { - return false; - } -} - -bool -ParticleSettingsWidget::on_mouse_motion(const SDL_MouseMotionEvent& motion) -{ - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(motion.x, motion.y); - - /*if (mouse_pos.x < SIZE && m_mouse_pos.y < SIZE) { - m_scrolling_vec = m_mouse_pos - Vector(MIDDLE, MIDDLE); - if (m_scrolling_vec.x != 0 || m_scrolling_vec.y != 0) { - float norm = m_scrolling_vec.norm(); - m_scrolling_vec *= powf(static_cast(M_E), norm / 16.0f - 1.0f); - } - }*/ - - m_hovering = get_bar_rect().contains(int(mouse_pos.x), int(mouse_pos.y)); - - int new_progress = m_progress + int((mouse_pos.y - last_mouse_pos) * VideoSystem::current()->get_viewport().get_scale().y * float(m_total_region) / float(m_covered_region)); - last_mouse_pos = mouse_pos.y; - - if (m_scrolling) { - - m_progress = std::min(m_total_region - m_covered_region, std::max(0, new_progress)); - - printf("%d to %d of %d\n", m_progress, m_progress + m_covered_region, m_total_region); - - return true; - } else { - return false; - } -} - -Rect -ParticleSettingsWidget::get_bar_rect() -{ - return Rect(m_rect.left, - m_rect.top + int(float(m_progress) - * float(m_covered_region) - / float(m_total_region) - ), - m_rect.right, - m_rect.top + int(float(m_progress) - * float(m_covered_region) - / float(m_total_region)) - + int(float(m_rect.get_height()) - * float(m_covered_region) - / float(m_total_region) - ) - ); -} -#endif -/* EOF */ diff --git a/src/editor/particle_settings_widget.hpp b/src/editor/particle_settings_widget.hpp deleted file mode 100644 index 8456df12a4d..00000000000 --- a/src/editor/particle_settings_widget.hpp +++ /dev/null @@ -1,80 +0,0 @@ -// SuperTux -// Copyright (C) 2020 A. Semphris -// -// 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 . - -#if 0 -#ifndef HEADER_SUPERTUX_EDITOR_PARTICLE_SETTINGS_WIDGET_HPP -#define HEADER_SUPERTUX_EDITOR_PARTICLE_SETTINGS_WIDGET_HPP - -#include "editor/widget.hpp" -#include "math/rect.hpp" -#include "math/vector.hpp" - -class DrawingContext; -union SDL_Event; - -/** A generic template for a scrollbar */ -class ParticleSettingsWidget final : public Widget -{ -public: - ParticleSettingsWidget(); - - virtual void draw(DrawingContext& context) override; - virtual void update(float dt_sec) override; - - virtual bool on_mouse_button_up(const SDL_MouseButtonEvent& button) override; - virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override; - virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override; - -private: - /** Whether or not the mouse is clicking on the bar */ - bool m_scrolling; - - /** Whether or not the mouse hovers above the bar */ - bool m_hovering; - - /** The length (height) of the region to scroll */ - int m_total_region; - - /** The length (height) of the viewport for the region */ - int m_covered_region; - - /** The length (height) between the beginning of the viewport and the beginning of the region */ - int m_progress; - - /** The logical position and size of the widget */ - Rect m_rect; - - /** The position and size of the widget, to scale */ - Rect m_scaled_rect; - - /** `true` of the scroller is horizontal; `false` if it is vertical */ - bool is_horizontal; - -private: - Rect get_bar_rect(); - - float last_mouse_pos; - float zoom_factor; - -private: - ParticleSettingsWidget(const ParticleSettingsWidget&) = delete; - ParticleSettingsWidget& operator=(const ParticleSettingsWidget&) = delete; -}; - -#endif -#endif - -/* EOF */ diff --git a/src/interface/control.hpp b/src/interface/control.hpp index f454b1d99d3..6418b5d8e6d 100644 --- a/src/interface/control.hpp +++ b/src/interface/control.hpp @@ -36,10 +36,10 @@ class InterfaceControl : public Widget virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override { if (m_label) m_label->on_mouse_motion(motion); return false; } void set_focus(bool focus) { m_has_focus = focus; } - bool has_focus() { return m_has_focus; } + bool has_focus() const { return m_has_focus; } void set_rect(Rectf rect) { m_rect = rect; } - Rectf get_rect() { return m_rect; } + Rectf get_rect() const { return m_rect; } public: /** Optional; a function that will be called each time the bound value diff --git a/src/interface/control_button.cpp b/src/interface/control_button.cpp index f7ac5b5e734..8bed3896042 100644 --- a/src/interface/control_button.cpp +++ b/src/interface/control_button.cpp @@ -49,27 +49,22 @@ ControlButton::draw(DrawingContext& context) bool ControlButton::on_mouse_button_up(const SDL_MouseButtonEvent& button) { - if (button.button == SDL_BUTTON_LEFT) { + if (button.button != SDL_BUTTON_LEFT) + return false; - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); - if (m_rect.contains(mouse_pos) && m_mouse_down) { + m_mouse_down = false; - if (m_on_change) - (*m_on_change)(); + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (!m_rect.contains(mouse_pos) || !m_mouse_down) { + return false; + } - m_has_focus = true; + if (m_on_change) + (*m_on_change)(); - m_mouse_down = false; - return true; - } else { - m_mouse_down = false; - return false; - } + m_has_focus = true; - } else { - m_mouse_down = false; - return false; - } + return true; } bool @@ -90,24 +85,30 @@ ControlButton::on_mouse_button_down(const SDL_MouseButtonEvent& button) bool ControlButton::on_key_up(const SDL_KeyboardEvent& key) { - if (key.keysym.sym == SDLK_SPACE && m_has_focus) { + if (!m_has_focus) + return false; + + if (key.keysym.sym == SDLK_SPACE) { if (m_on_change) (*m_on_change)(); m_mouse_down = false; return true; - } else { - return false; } + + return false; } bool ControlButton::on_key_down(const SDL_KeyboardEvent& key) { - if (key.keysym.sym == SDLK_SPACE && m_has_focus) { + if (!m_has_focus) + return false; + + if (key.keysym.sym == SDLK_SPACE) { m_mouse_down = true; return true; - } else { - return false; } + + return false; } diff --git a/src/interface/control_checkbox.cpp b/src/interface/control_checkbox.cpp index 49c93c11ebb..bec35d93c09 100644 --- a/src/interface/control_checkbox.cpp +++ b/src/interface/control_checkbox.cpp @@ -48,25 +48,22 @@ ControlCheckbox::draw(DrawingContext& context) bool ControlCheckbox::on_mouse_button_up(const SDL_MouseButtonEvent& button) { - if (button.button == SDL_BUTTON_LEFT) { + if (button.button != SDL_BUTTON_LEFT) + return false; - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); - if (m_rect.contains(mouse_pos)) { - *m_value = !*m_value; + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); - if (m_on_change) - (*m_on_change)(); + if (!m_rect.contains(mouse_pos)) + return false; - m_has_focus = true; + *m_value = !*m_value; - return true; - } else { - return false; - } + if (m_on_change) + (*m_on_change)(); - } else { - return false; - } + m_has_focus = true; + + return true; } bool @@ -82,15 +79,14 @@ ControlCheckbox::on_mouse_button_down(const SDL_MouseButtonEvent& button) bool ControlCheckbox::on_key_up(const SDL_KeyboardEvent& key) { - if (key.keysym.sym == SDLK_SPACE && m_has_focus) { - *m_value = !*m_value; + if (key.keysym.sym != SDLK_SPACE || !m_has_focus) + return false; - if (m_on_change) - (*m_on_change)(); + *m_value = !*m_value; - return true; - } else { - return false; - } + if (m_on_change) + (*m_on_change)(); + + return true; } diff --git a/src/interface/control_checkbox.hpp b/src/interface/control_checkbox.hpp index 62a46bb60ec..5bb1af21747 100644 --- a/src/interface/control_checkbox.hpp +++ b/src/interface/control_checkbox.hpp @@ -29,7 +29,7 @@ class ControlCheckbox : public InterfaceControl virtual bool on_mouse_button_down(const SDL_MouseButtonEvent& button) override; virtual bool on_key_up(const SDL_KeyboardEvent& key) override; - bool get_value() { return *m_value; } + bool get_value() const { return *m_value; } void set_value(bool value) { *m_value = value; } void bind_value(bool* value) { m_value = value; } diff --git a/src/interface/control_enum.cpp b/src/interface/control_enum.cpp index 8cd582d4299..0f7cb9f0c56 100644 --- a/src/interface/control_enum.cpp +++ b/src/interface/control_enum.cpp @@ -16,7 +16,7 @@ #include "interface/control_enum.hpp" -// The source is in the header file - I can put it in the source file +// The source is in the header file - I can't put it in the source file // because of this bug : // https://stackoverflow.com/questions/56041900 diff --git a/src/interface/control_enum.hpp b/src/interface/control_enum.hpp index 0db299a640e..32791cdac0f 100644 --- a/src/interface/control_enum.hpp +++ b/src/interface/control_enum.hpp @@ -34,7 +34,7 @@ class ControlEnum : public InterfaceControl virtual bool on_key_up(const SDL_KeyboardEvent& key) override; virtual bool on_key_down(const SDL_KeyboardEvent& key) override; - T get_value() { return *m_value; } + T get_value() const { return *m_value; } void set_value(T value) { *m_value = value; } void bind_value(T* value) { m_value = value; } @@ -141,21 +141,20 @@ template bool ControlEnum::on_mouse_button_up(const SDL_MouseButtonEvent& button) { - if (button.button == SDL_BUTTON_LEFT) { - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); - if (m_rect.contains(mouse_pos)) { - m_open_list = !m_open_list; - m_has_focus = true; - return true; - } else if (Rectf(m_rect.get_left(), - m_rect.get_top(), - m_rect.get_right(), - m_rect.get_bottom() + m_rect.get_height() * float(m_options.size()) - ).contains(mouse_pos) && m_open_list) { - return true; - } else { - return false; - } + if (button.button != SDL_BUTTON_LEFT) + return false; + + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); + if (m_rect.contains(mouse_pos)) { + m_open_list = !m_open_list; + m_has_focus = true; + return true; + } else if (Rectf(m_rect.get_left(), + m_rect.get_top(), + m_rect.get_right(), + m_rect.get_bottom() + m_rect.get_height() * float(m_options.size()) + ).contains(mouse_pos) && m_open_list) { + return true; } else { return false; } @@ -234,7 +233,10 @@ template bool ControlEnum::on_key_down(const SDL_KeyboardEvent& key) { - if (key.keysym.sym == SDLK_DOWN && m_has_focus) { + if (!m_has_focus) + return false; + + if (key.keysym.sym == SDLK_DOWN) { bool is_next = false; // Hacky way to get the next one in the list for (auto option : m_options) { @@ -260,7 +262,7 @@ ControlEnum::on_key_down(const SDL_KeyboardEvent& key) (*m_on_change)(); return true; - } else if (key.keysym.sym == SDLK_UP && m_has_focus) { + } else if (key.keysym.sym == SDLK_UP) { bool is_last = false; bool currently_on_first = true; @@ -286,9 +288,9 @@ ControlEnum::on_key_down(const SDL_KeyboardEvent& key) (*m_on_change)(); return true; - } else { - return false; } + + return false; } #endif diff --git a/src/interface/control_textbox.cpp b/src/interface/control_textbox.cpp index 02ea1ccb660..dbebdbb7e48 100644 --- a/src/interface/control_textbox.cpp +++ b/src/interface/control_textbox.cpp @@ -353,13 +353,13 @@ ControlTextbox::revert_value() } std::string -ControlTextbox::get_string() +ControlTextbox::get_string() const { return m_internal_string_backup; } std::string -ControlTextbox::get_contents() +ControlTextbox::get_contents() const { std::string temp; @@ -372,7 +372,7 @@ ControlTextbox::get_contents() } std::string -ControlTextbox::get_first_chars(int amount) +ControlTextbox::get_first_chars(int amount) const { std::string temp; @@ -386,7 +386,7 @@ ControlTextbox::get_first_chars(int amount) } std::string -ControlTextbox::get_contents_visible() +ControlTextbox::get_contents_visible() const { std::string temp; int remaining = m_current_offset; @@ -402,13 +402,13 @@ ControlTextbox::get_contents_visible() } std::string -ControlTextbox::get_first_chars_visible(int amount) +ControlTextbox::get_first_chars_visible(int amount) const { return get_contents_visible().substr(0, amount); } int -ControlTextbox::get_text_position(Vector pos) +ControlTextbox::get_text_position(Vector pos) const { float dist = pos.x - m_rect.get_left(); int i = 0; @@ -421,7 +421,7 @@ ControlTextbox::get_text_position(Vector pos) } std::string -ControlTextbox::get_truncated_text(std::string text) +ControlTextbox::get_truncated_text(std::string text) const { if (fits(text)) return text; @@ -433,7 +433,7 @@ ControlTextbox::get_truncated_text(std::string text) } bool -ControlTextbox::fits(std::string text) +ControlTextbox::fits(std::string text) const { return Resources::control_font->get_text_width(text) <= m_rect.get_width() - 10.f; } diff --git a/src/interface/control_textbox.hpp b/src/interface/control_textbox.hpp index 966a44b6a27..54df039288e 100644 --- a/src/interface/control_textbox.hpp +++ b/src/interface/control_textbox.hpp @@ -42,13 +42,13 @@ class ControlTextbox : public InterfaceControl void bind_string(std::string* value) { m_string = value; } /** Returns the full string held in m_charlist */ - std::string get_string(); + std::string get_string() const; /** Gets at which (absolute) index, in the text, corresponds an on-screen point */ - int get_text_position(Vector pos); + int get_text_position(Vector pos) const; /** Returns true if the given text would fit inside the box */ - bool fits(std::string text); + bool fits(std::string text) const; protected: /** Transfers the string into the binded variable, if any. Can be overridden @@ -85,16 +85,16 @@ class ControlTextbox : public InterfaceControl */ /** Converts the internal char vector to an actual string and returns it. */ - std::string get_contents(); + std::string get_contents() const; /** Returns first "amount" chars held in m_charlist */ - std::string get_first_chars(int amount); + std::string get_first_chars(int amount) const; /** Returns the part of the string that is actually displayed */ - std::string get_contents_visible(); + std::string get_contents_visible() const; /** Returns first "amount" chars that are displayed */ - std::string get_first_chars_visible(int amount); + std::string get_first_chars_visible(int amount) const; public: /** Optional, a function to validate the string. If nullptr, then all values @@ -147,7 +147,7 @@ class ControlTextbox : public InterfaceControl void on_backspace(); /** Returns the largest string fitting in the box. */ - std::string get_truncated_text(std::string text); + std::string get_truncated_text(std::string text) const; /** Changes m_current_offset so the the caret is visible */ void recenter_offset(); diff --git a/src/interface/control_textbox_float.hpp b/src/interface/control_textbox_float.hpp index 56dc7cca172..9bab6012393 100644 --- a/src/interface/control_textbox_float.hpp +++ b/src/interface/control_textbox_float.hpp @@ -26,7 +26,7 @@ class ControlTextboxFloat : public ControlTextbox virtual void update(float dt_sec, const Controller& controller) override; - float get_value() { return *m_value; } + float get_value() const { return *m_value; } void set_value(float value) { *m_value = value; revert_value(); } /** Binds a float to this textbox. Set m_validate_float(float) if you want * custom validation. (You may also use m_validate_string(string), though diff --git a/src/interface/control_textbox_int.hpp b/src/interface/control_textbox_int.hpp index f7b58939db3..292643678d2 100644 --- a/src/interface/control_textbox_int.hpp +++ b/src/interface/control_textbox_int.hpp @@ -26,7 +26,7 @@ class ControlTextboxInt : public ControlTextbox virtual void update(float dt_sec, const Controller& controller) override; - int get_value() { return *m_value; } + int get_value() const { return *m_value; } void set_value(int value) { *m_value = value; revert_value(); } /** Binds an int to this textbox. Set m_validate_fint(int) if you want * custom validation. (You may also use m_validate_string(string), though diff --git a/src/interface/label.cpp b/src/interface/label.cpp index 7fc85e38955..e8852de6335 100644 --- a/src/interface/label.cpp +++ b/src/interface/label.cpp @@ -78,13 +78,13 @@ InterfaceLabel::draw(DrawingContext& context) } bool -InterfaceLabel::fits(std::string text) +InterfaceLabel::fits(std::string text) const { return Resources::control_font->get_text_width(text) <= m_rect.get_width(); } std::string -InterfaceLabel::get_truncated_text() +InterfaceLabel::get_truncated_text() const { if (fits(m_label)) return m_label; diff --git a/src/interface/label.hpp b/src/interface/label.hpp index 8d2043ac1df..d34168245a3 100644 --- a/src/interface/label.hpp +++ b/src/interface/label.hpp @@ -33,13 +33,13 @@ class InterfaceLabel : public Widget virtual bool on_mouse_motion(const SDL_MouseMotionEvent& motion) override; void set_rect(Rectf rect) { m_rect = rect; } - Rectf get_rect() { return m_rect; } + Rectf get_rect() const { return m_rect; } void set_label(std::string label) { m_label = label; } - std::string get_label() { return m_label; } + std::string get_label() const { return m_label; } - bool fits(std::string text); - std::string get_truncated_text(); + bool fits(std::string text) const; + std::string get_truncated_text() const; protected: /** The rectangle where the InterfaceLabel should be rendered */ diff --git a/src/object/custom_particle_system.cpp b/src/object/custom_particle_system.cpp index 9d0cef59f12..63b523fcdb9 100644 --- a/src/object/custom_particle_system.cpp +++ b/src/object/custom_particle_system.cpp @@ -41,8 +41,6 @@ #include "video/video_system.hpp" #include "video/viewport.hpp" -#define PI 3.1415926535897f - CustomParticleSystem::CustomParticleSystem() : ExposedObject(this), texture_sum_odds(0.f), @@ -610,7 +608,7 @@ CustomParticleSystem::update(float dt_sec) particle->ready_for_deletion = true; } } - + particle->speedX += graphicsRandom.randf(-particle->feather_factor, particle->feather_factor) * dt_sec * 1000.f; particle->speedY += graphicsRandom.randf(-particle->feather_factor, @@ -619,7 +617,7 @@ CustomParticleSystem::update(float dt_sec) particle->speedY += particle->accY * dt_sec; particle->speedX *= 1.f - particle->frictionX * dt_sec; particle->speedY *= 1.f - particle->frictionY * dt_sec; - + if (Sector::current() && collision(particle, Vector(particle->speedX,particle->speedY) * dt_sec) > 0) { switch(particle->collision_mode) { @@ -628,13 +626,50 @@ CustomParticleSystem::update(float dt_sec) particle->pos.y += particle->speedY * dt_sec; break; case CollisionMode::Stick: - + // Just don't move break; case CollisionMode::BounceHeavy: - - break; case CollisionMode::BounceLight: - + { + auto c = get_collision(particle, Vector(particle->speedX, particle->speedY) * dt_sec); + + float speed_angle = atan(-particle->speedY / particle->speedX); + float face_angle = atan(c.slope_normal.y / c.slope_normal.x); + if (c.slope_normal.x == 0.f && c.slope_normal.y == 0.f) { + auto cX = get_collision(particle, Vector(particle->speedX, 0) * dt_sec); + if (cX.left != cX.right) + particle->speedX *= -1; + auto cY = get_collision(particle, Vector(0, particle->speedY) * dt_sec); + if (cY.top != cY.bottom) + particle->speedY *= -1; + } else { + float dest_angle = face_angle * 2.f - speed_angle; // Reflect the angle around face_angle + float dX = cos(dest_angle), + dY = sin(dest_angle); + + float true_speed = static_cast(sqrt(pow(particle->speedY, 2) + + pow(particle->speedX, 2))); + + particle->speedX = dX * true_speed; + particle->speedY = dY * true_speed; + } + + switch(particle->collision_mode) { + case CollisionMode::BounceHeavy: + particle->speedX *= .2f; + particle->speedY *= .2f; + break; + case CollisionMode::BounceLight: + particle->speedX *= .7f; + particle->speedY *= .7f; + break; + default: + assert(false); + } + + particle->pos.x += particle->speedX * dt_sec; + particle->pos.y += particle->speedY * dt_sec; + } break; case CollisionMode::Destroy: particle->ready_for_deletion = true; @@ -647,7 +682,7 @@ CustomParticleSystem::update(float dt_sec) switch(particle->angle_mode) { case RotationMode::Facing: - particle->angle = atan(particle->speedY / particle->speedX) * 180.f / PI; + particle->angle = atan(particle->speedY / particle->speedX) * 180.f / math::PI; break; case RotationMode::Wiggling: particle->angle += graphicsRandom.randf(-particle->angle_speed / 2.f, @@ -833,7 +868,6 @@ CustomParticleSystem::draw(DrawingContext& context) for(auto& it : batches) { auto& surface = it.first->texture; auto& batch = it.second; - // FIXME: What is the colour used for? context.color().draw_surface_batch(surface, batch.move_srcrects(), batch.move_dstrects(), batch.move_angles(), it.first->color, z_pos); } @@ -884,6 +918,7 @@ CustomParticleSystem::collision(Particle* object, const Vector& movement) for (const auto& solids : Sector::get().get_solid_tilemaps()) { // FIXME Handle a nonzero tilemap offset + // Check if it gets fixed in particlesystem_interactive.cpp for (int x = starttilex; x*32 < max_x; ++x) { for (int y = starttiley; y*32 < max_y; ++y) { const Tile& tile = solids->get_tile(x, y); @@ -929,6 +964,71 @@ CustomParticleSystem::collision(Particle* object, const Vector& movement) } } +CollisionHit +CustomParticleSystem::get_collision(Particle* object, const Vector& movement) +{ + using namespace collision; + + CustomParticle* particle = dynamic_cast(object); + assert(particle); + + // calculate rectangle where the object will move + float x1, x2; + float y1, y2; + + x1 = object->pos.x - particle->props.scale.x * static_cast(particle->props.texture->get_width()) / 2; + x2 = x1 + particle->props.scale.x * static_cast(particle->props.texture->get_width()) + movement.x; + if (x2 < x1) { + float temp_x = x1; + x1 = x2; + x2 = temp_x; + } + + y1 = object->pos.y - particle->props.scale.y * static_cast(particle->props.texture->get_height()) / 2; + y2 = y1 + particle->props.scale.y * static_cast(particle->props.texture->get_height()) + movement.y; + if (y2 < y1) { + float temp_y = y1; + y1 = y2; + y2 = temp_y; + } + + // test with all tiles in this rectangle + int starttilex = int(x1-1) / 32; + int starttiley = int(y1-1) / 32; + int max_x = int(x2+1); + int max_y = int(y2+1); + + Rectf dest(x1, y1, x2, y2); + dest.move(movement); + Constraints constraints; + + for (const auto& solids : Sector::get().get_solid_tilemaps()) { + // FIXME Handle a nonzero tilemap offset + // Check if it gets fixed in particlesystem_interactive.cpp + for (int x = starttilex; x*32 < max_x; ++x) { + for (int y = starttiley; y*32 < max_y; ++y) { + const Tile& tile = solids->get_tile(x, y); + + // skip non-solid tiles + if (! (tile.get_attributes() & (/*Tile::WATER |*/ Tile::SOLID))) + continue; + + Rectf rect = solids->get_tile_bbox(x, y); + if (tile.is_slope ()) { // slope tile + AATriangle triangle = AATriangle(rect, tile.get_data()); + rectangle_aatriangle(&constraints, dest, triangle); + } else { // normal rectangular tile + if (intersects(dest, rect)) { + set_rectangle_rectangle_constraints(&constraints, dest, rect); + } + } + } + } + } + + return constraints.hit; +} + // ============================================================================= // LOCAL diff --git a/src/object/custom_particle_system.hpp b/src/object/custom_particle_system.hpp index 46ef59050bf..f406b184c78 100644 --- a/src/object/custom_particle_system.hpp +++ b/src/object/custom_particle_system.hpp @@ -52,6 +52,7 @@ class CustomParticleSystem : //void fade_amount(int new_amount, float fade_time); protected: virtual int collision(Particle* particle, const Vector& movement) override; + CollisionHit get_collision(Particle* particle, const Vector& movement); private: @@ -122,7 +123,7 @@ class CustomParticleSystem : { } - SpriteProperties(SpriteProperties& sp, float alpha) : + SpriteProperties(const SpriteProperties& sp, float alpha) : likeliness(sp.likeliness), color(sp.color.red, sp.color.green, sp.color.blue, sp.color.alpha * alpha), texture(sp.texture), @@ -316,9 +317,9 @@ class CustomParticleSystem : } }; - std::shared_ptr get_props() const + std::unique_ptr get_props() const { - std::shared_ptr props = std::make_shared(); + std::unique_ptr props = std::make_unique(); for (auto texture : m_textures) props->m_textures.push_back(texture); @@ -358,7 +359,7 @@ class CustomParticleSystem : return props; } - void set_props(std::shared_ptr props) + void set_props(ParticleProps* props) { m_textures.clear(); for (auto texture : props->m_textures) diff --git a/src/object/custom_particle_system_file.cpp b/src/object/custom_particle_system_file.cpp index 35a77e7e612..93012a756a4 100644 --- a/src/object/custom_particle_system_file.cpp +++ b/src/object/custom_particle_system_file.cpp @@ -16,6 +16,7 @@ #include "object/custom_particle_system_file.hpp" +#include "editor/editor.hpp" #include "gui/menu_manager.hpp" #include "util/reader.hpp" #include "util/reader_document.hpp" @@ -49,6 +50,9 @@ CustomParticleSystemFile::get_settings() result.add_file(_("File"), &m_filename, "file", {}, {".stcp"}, "/particles"); result.add_particle_editor(); + // It is assumed get_settings() is called whenever the menu is opened + Editor::current()->m_particle_editor_filename = &m_filename; + result.add_remove(); return result; @@ -66,7 +70,7 @@ CustomParticleSystemFile::update_data() if (root.get_name() != "supertux-custom-particle") throw std::runtime_error("file is not a supertux-custom-particle file."); - set_props(CustomParticleSystem(mapping).get_props()); + set_props(CustomParticleSystem(mapping).get_props().get()); } /* EOF */ diff --git a/src/supertux/menu/particle_editor_save_as.cpp b/src/supertux/menu/particle_editor_save_as.cpp index 8448c0e675e..cd295358640 100644 --- a/src/supertux/menu/particle_editor_save_as.cpp +++ b/src/supertux/menu/particle_editor_save_as.cpp @@ -1,5 +1,5 @@ // SuperTux -// Copyright (C) 2015 Hume2 +// Copyright (C) 2020 A. Semphris // // 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 diff --git a/src/supertux/menu/particle_editor_save_as.hpp b/src/supertux/menu/particle_editor_save_as.hpp index 7058d4ba08f..1d54a3381a5 100644 --- a/src/supertux/menu/particle_editor_save_as.hpp +++ b/src/supertux/menu/particle_editor_save_as.hpp @@ -1,5 +1,5 @@ // SuperTux -// Copyright (C) 2015 Hume2 +// Copyright (C) 2020 A. Semphris // // 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 diff --git a/src/video/surface_batch.hpp b/src/video/surface_batch.hpp index 58fb623cb4d..245aa42e8bd 100644 --- a/src/video/surface_batch.hpp +++ b/src/video/surface_batch.hpp @@ -39,7 +39,7 @@ class SurfaceBatch std::vector move_dstrects() { return std::move(m_dstrects); } std::vector move_angles() { return std::move(m_angles); } - Color get_color() { return m_color; } + Color get_color() const { return m_color; } private: SurfacePtr m_surface; From a815584b562baf685e8b82da7955933d5ddf8f84 Mon Sep 17 00:00:00 2001 From: Semphris Date: Wed, 18 Nov 2020 01:27:42 -0500 Subject: [PATCH 8/9] Fixed some last-minute bugs (Interface buttons not working and undo not managing textures) --- src/editor/particle_editor.cpp | 13 +++++++++++++ src/interface/control_button.cpp | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/editor/particle_editor.cpp b/src/editor/particle_editor.cpp index bc94107c3b3..88362441ddf 100644 --- a/src/editor/particle_editor.cpp +++ b/src/editor/particle_editor.cpp @@ -335,6 +335,7 @@ ParticleEditor::reset_texture_ui() [this](std::string new_filename) { (m_particles->m_textures.begin() + m_texture_current)->texture = Surface::from_file(new_filename); m_particles->reinit_textures(); + this->push_version(); } )); }); @@ -361,6 +362,8 @@ ParticleEditor::reset_texture_ui() if (m_texture_current < 0) m_texture_current = 0; for (const auto& refresh : m_texture_rebinds) refresh(); + m_particles->reinit_textures(); + this->push_version(); }); del_btn.get()->set_rect(Rectf(150.f, 400, 170.f, 420.f)); m_controls_textures.push_back(std::move(del_btn)); @@ -371,6 +374,8 @@ ParticleEditor::reset_texture_ui() m_texture_current = static_cast(m_particles->m_textures.size()) - 1; for (const auto& refresh : m_texture_rebinds) refresh(); + m_particles->reinit_textures(); + this->push_version(); }); add_btn.get()->set_rect(Rectf(190.f, 400, 210.f, 420.f)); m_controls_textures.push_back(std::move(add_btn)); @@ -810,6 +815,10 @@ ParticleEditor::undo() m_redo_stack.push_back(m_undo_stack.back()); m_undo_stack.pop_back(); m_particles->set_props(m_undo_stack.back().get()); + + m_particles->reinit_textures(); + if (m_texture_current > static_cast(m_particles->m_textures.size()) - 1) + m_texture_current = static_cast(m_particles->m_textures.size()) - 1; } void @@ -821,6 +830,10 @@ ParticleEditor::redo() m_undo_stack.push_back(m_redo_stack.back()); m_particles->set_props(m_redo_stack.back().get()); m_redo_stack.pop_back(); + + m_particles->reinit_textures(); + if (m_texture_current > static_cast(m_particles->m_textures.size()) - 1) + m_texture_current = static_cast(m_particles->m_textures.size()) - 1; } /* EOF */ diff --git a/src/interface/control_button.cpp b/src/interface/control_button.cpp index 8bed3896042..ec735c43e0b 100644 --- a/src/interface/control_button.cpp +++ b/src/interface/control_button.cpp @@ -52,13 +52,14 @@ ControlButton::on_mouse_button_up(const SDL_MouseButtonEvent& button) if (button.button != SDL_BUTTON_LEFT) return false; - m_mouse_down = false; - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(button.x, button.y); if (!m_rect.contains(mouse_pos) || !m_mouse_down) { + m_mouse_down = false; return false; } + m_mouse_down = false; + if (m_on_change) (*m_on_change)(); From bf46d142af09f0c6afe09f29a8b2241d421528d9 Mon Sep 17 00:00:00 2001 From: Semphris Date: Wed, 18 Nov 2020 11:53:00 -0500 Subject: [PATCH 9/9] Fxed missing files and added some stability with a try-catch in case of I/O errors --- CMakeLists.txt | 1 + src/object/custom_particle_system_file.cpp | 23 ++++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 07c603cce47..27970006297 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -980,6 +980,7 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/images ${CMAKE_CURRENT_SOURCE_DIR}/data/fonts ${CMAKE_CURRENT_SOURCE_DIR}/data/music + ${CMAKE_CURRENT_SOURCE_DIR}/data/particles ${CMAKE_CURRENT_SOURCE_DIR}/data/scripts ${CMAKE_CURRENT_SOURCE_DIR}/data/shader ${CMAKE_CURRENT_SOURCE_DIR}/data/speech diff --git a/src/object/custom_particle_system_file.cpp b/src/object/custom_particle_system_file.cpp index 93012a756a4..b01f80ced86 100644 --- a/src/object/custom_particle_system_file.cpp +++ b/src/object/custom_particle_system_file.cpp @@ -61,16 +61,23 @@ CustomParticleSystemFile::get_settings() void CustomParticleSystemFile::update_data() { - // FIXME: Add a try-catch in case of I/O error (in case we switch to a dialog - // that doesn't filter strictly) - auto doc = ReaderDocument::from_file("particles/" + ((m_filename == "") ? "default.stcp" : m_filename)); - auto root = doc.get_root(); - auto mapping = root.get_mapping(); + try + { + auto doc = ReaderDocument::from_file("particles/" + ((m_filename == "") ? "default.stcp" : m_filename)); + auto root = doc.get_root(); + auto mapping = root.get_mapping(); - if (root.get_name() != "supertux-custom-particle") - throw std::runtime_error("file is not a supertux-custom-particle file."); + if (root.get_name() != "supertux-custom-particle") + throw std::runtime_error("file is not a supertux-custom-particle file."); - set_props(CustomParticleSystem(mapping).get_props().get()); + set_props(CustomParticleSystem(mapping).get_props().get()); + } + catch (std::exception& e) + { + log_warning << "Could not update custom particle from file (fallback to default settings): " + << e.what() << std::endl; + set_props(CustomParticleSystem().get_props().get()); + } } /* EOF */