diff --git a/data/images/particles/air_bubble-0.png b/data/images/particles/air_bubble-0.png new file mode 100644 index 00000000000..368f9bed013 Binary files /dev/null and b/data/images/particles/air_bubble-0.png differ diff --git a/data/images/particles/air_bubble-1.png b/data/images/particles/air_bubble-1.png new file mode 100644 index 00000000000..c7a03db4047 Binary files /dev/null and b/data/images/particles/air_bubble-1.png differ diff --git a/data/images/particles/air_bubble-2.png b/data/images/particles/air_bubble-2.png new file mode 100644 index 00000000000..bfc9980b1f3 Binary files /dev/null and b/data/images/particles/air_bubble-2.png differ diff --git a/data/images/particles/air_bubble-3.png b/data/images/particles/air_bubble-3.png new file mode 100644 index 00000000000..cf99200699f Binary files /dev/null and b/data/images/particles/air_bubble-3.png differ diff --git a/data/images/particles/air_bubble-4.png b/data/images/particles/air_bubble-4.png new file mode 100644 index 00000000000..b5195aeda4b Binary files /dev/null and b/data/images/particles/air_bubble-4.png differ diff --git a/data/images/particles/air_bubble.sprite b/data/images/particles/air_bubble.sprite new file mode 100644 index 00000000000..4c51ad33204 --- /dev/null +++ b/data/images/particles/air_bubble.sprite @@ -0,0 +1,23 @@ +(supertux-sprite + (action + (name "normal") + (fps 15) + (loops 1) + (images "air_bubble-0.png" + "air_bubble-1.png" + "air_bubble-2.png" + "air_bubble-3.png" + "air_bubble-4.png" + ) + ) + (action + (name "small") + (fps 15) + (loops 1) + (images "air_bubble-0.png" + "air_bubble-1.png" + "air_bubble-2.png" + "air_bubble-3.png" + ) + ) +) diff --git a/src/object/player.cpp b/src/object/player.cpp index 5254fb2cf10..30f55b1aa64 100644 --- a/src/object/player.cpp +++ b/src/object/player.cpp @@ -144,6 +144,7 @@ const float SWIM_BOOST_SPEED = 600.f; const float SWIM_TO_BOOST_ACCEL = 15.f; const float TURN_MAGNITUDE = 0.15f; const float TURN_MAGNITUDE_BOOST = 0.2f; +const std::array BUBBLE_ACTIONS = { "normal", "small" }; /* Buttjump variables */ @@ -226,6 +227,7 @@ Player::Player(PlayerStatus& player_status, const std::string& name_, int player m_swimming_accel_modifier(100.f), m_water_jump(false), m_airarrow(Surface::from_file("images/engine/hud/airarrow.png")), + m_bubbles_sprite(SpriteManager::current()->create("images/particles/air_bubble.sprite")), m_floor_normal(0.0f, 0.0f), m_ghost_mode(false), m_unduck_hurt_timer(), @@ -254,6 +256,8 @@ Player::Player(PlayerStatus& player_status, const std::string& name_, int player SoundManager::current()->preload("sounds/invincible_start.ogg"); SoundManager::current()->preload("sounds/splash.wav"); SoundManager::current()->preload("sounds/grow.wav"); + m_bubble_timer.start(3.0f + graphicsRandom.randf(2)); + m_col.set_size(TUX_WIDTH, is_big() ? BIG_TUX_HEIGHT : SMALL_TUX_HEIGHT); m_sprite->set_angle(0.0f); @@ -412,6 +416,23 @@ Player::update(float dt_sec) } } + if (!m_active_bubbles.empty()) + { + for (auto& bubble : m_active_bubbles) + { + bubble.second.y -= dt_sec * 40.0f; + bubble.second.x += std::sin(bubble.second.y * 0.1f) * dt_sec * 5.0f; + } + + m_active_bubbles.remove_if([&](const std::pair& bubble) + { + Rectf bubble_box(bubble.second.x, bubble.second.y, bubble.second.x + 16.f, bubble.second.y + 16.f); + bool is_out_of_water = Sector::get().is_free_of_tiles(bubble_box, true, Tile::WATER); + bool hits_solid = !Sector::get().is_free_of_tiles(bubble_box, false, Tile::SOLID); + return is_out_of_water || hits_solid; + }); + } + // Skip if in multiplayer respawn if (is_dead() && m_target && Sector::get().get_object_count([this](const Player& p) { return !p.is_dead() && !p.is_dying() && !p.is_winning() && &p != this; })) { @@ -514,6 +535,71 @@ Player::update(float dt_sec) if (m_physic.get_velocity_y() > -350.f && m_controller->hold(Control::UP)) m_physic.set_velocity_y(-350.f); } + + if (m_bubble_timer.check()) + { + Vector beak_local_offset(30.f, 0.0f); + float big_offset_x = is_big() ? 4.0f : 0.0f; + + // Calculate the offsets based on the sprite angle + float offset_x = std::cos(m_swimming_angle) * 10.0f; + float offset_y = std::sin(m_swimming_angle) * 10.0f; + + // Rotate the beak offset based on the sprite's angle + float rotated_beak_offset_x = beak_local_offset.x * std::cos(m_swimming_angle) - beak_local_offset.y * std::sin(m_swimming_angle); + float rotated_beak_offset_y = beak_local_offset.x * std::sin(m_swimming_angle) + beak_local_offset.y * std::cos(m_swimming_angle); + + Vector player_center = m_col.m_bbox.get_middle(); + Vector beak_position; + + // Determine direction based on the radians + if (std::abs(m_swimming_angle) > static_cast(math::PI_2)) // Facing left + { + beak_position = player_center + Vector(rotated_beak_offset_x - big_offset_x * 2, rotated_beak_offset_y); + } + else // Facing right (including straight up or down) + { + beak_position = player_center + Vector(rotated_beak_offset_x - 4.0f + big_offset_x, rotated_beak_offset_y); + } + + int num_bubbles = graphicsRandom.rand(1, 3); + + for (int i = 0; i < num_bubbles; ++i) + { + int random_action_index = graphicsRandom.rand(0, 2); + std::string selected_action = BUBBLE_ACTIONS[random_action_index]; + + SpritePtr bubble_sprite = m_bubbles_sprite->clone(); + bubble_sprite->set_animation_loops(1); + bubble_sprite->set_action(selected_action); + + Vector bubble_pos(0.f, 0.f); + if (std::abs(m_swimming_angle) > static_cast(math::PI_2)) // Facing left + { + bubble_pos = beak_position + Vector(offset_x - big_offset_x * 2, offset_y); + } + else // Facing right (including straight up or down) + { + bubble_pos = beak_position + Vector(offset_x - 4.0f + big_offset_x, offset_y); + } + + if (num_bubbles > 1) + { + float burst_offset_x = graphicsRandom.randf(-5.0f, 5.0f); + float burst_offset_y = graphicsRandom.randf(-5.0f, 5.0f); + bubble_pos.x += burst_offset_x; + bubble_pos.y += burst_offset_y; + } + + if (bubble_pos.y > -1) + { + m_active_bubbles.emplace_back(std::make_pair(std::move(bubble_sprite), bubble_pos)); + } + } + + // Restart the timer for the next wave of bubbles + m_bubble_timer.start(0.8f + graphicsRandom.randf(0.0f, 0.6f)); + } } else { @@ -2245,10 +2331,13 @@ Player::draw(DrawingContext& context) get_bonus() == EARTH_BONUS ? Color(1.f, 0.9f, 0.6f) : Color(1.f, 1.f, 1.f)); + for (auto& bubble_sprite : m_active_bubbles) + { + bubble_sprite.first->draw(context.color(), bubble_sprite.second, LAYER_TILES - 5); + } m_sprite->set_color(m_stone ? Color(1.f, 1.f, 1.f) : power_color); } - void Player::collision_tile(uint32_t tile_attributes) { diff --git a/src/object/player.hpp b/src/object/player.hpp index 984dbaa5c4d..89bea47913f 100644 --- a/src/object/player.hpp +++ b/src/object/player.hpp @@ -28,6 +28,9 @@ #include "video/layer.hpp" #include "video/surface_ptr.hpp" +#include +#include + class BadGuy; class Climbable; class Controller; @@ -552,6 +555,10 @@ class Player final : public MovingObject SurfacePtr m_airarrow; /**< arrow indicating Tux' position when he's above the camera */ + SpritePtr m_bubbles_sprite; /**< bubble particles sprite for swimming */ + Timer m_bubble_timer; /**< timer for spawning bubble particles */ + std::list> m_active_bubbles; /**< active bubble particles */ + Vector m_floor_normal; bool m_ghost_mode; /**< indicates if Tux should float around and through solid objects */