diff --git a/data/shaders/speed_lines.frag b/data/shaders/speed_lines.frag new file mode 100644 index 00000000000..f83db45614f --- /dev/null +++ b/data/shaders/speed_lines.frag @@ -0,0 +1,141 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2024 SuperTuxKart-Team +// +// 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, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +// speed_lines.frag +// Creates radial speed lines effect during acceleration/boost + +// Speed intensity [0.0, 1.0] - controls line density and length +uniform float speed_intensity; + +// Boost intensity [0.0, 1.0] - for nitro/zipper effects (adds purple tint) +uniform float boost_intensity; + +// Time for animation +uniform float time; + +// The color buffer +uniform sampler2D color_buffer; + +// Center point (in UV coordinates) - where the kart/driver is +uniform vec2 center; + +// Inner radius where lines start to fade in +uniform float inner_radius; + +out vec4 FragColor; + +// Pseudo-random function +float hash(vec2 p) +{ + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +// Generate speed line pattern +float speedLine(vec2 uv, vec2 center_point, float angle_offset) +{ + vec2 dir = uv - center_point; + float dist = length(dir); + + // Calculate angle from center + float angle = atan(dir.y, dir.x); + + // Number of lines around the circle + float num_lines = 60.0 + speed_intensity * 40.0; + + // Create angular sections + float section = floor((angle + 3.14159) / (6.28318 / num_lines)); + + // Random offset for each line to make them non-uniform + float rand_offset = hash(vec2(section, floor(time * 2.0))); + + // Line position within its section + float line_pos = fract((angle + 3.14159) / (6.28318 / num_lines)); + + // Create thin lines (using smoothstep for anti-aliasing) + float line_width = 0.08 + rand_offset * 0.04; + float line = smoothstep(0.5 - line_width, 0.5, line_pos) * + smoothstep(0.5 + line_width, 0.5, line_pos); + + // Radial falloff - lines are stronger at the edges, fade near center + float radial = smoothstep(inner_radius, inner_radius + 0.2, dist); + + // Lines get longer/more visible at edges of screen + float edge_intensity = smoothstep(0.3, 0.8, dist); + + // Animate lines moving outward + float anim_offset = fract(time * (1.0 + speed_intensity) + rand_offset); + float anim_fade = smoothstep(0.0, 0.3, anim_offset) * smoothstep(1.0, 0.7, anim_offset); + + // Random visibility per line (some lines appear, some don't) + float visibility = step(0.3 - speed_intensity * 0.25, hash(vec2(section, floor(time * 3.0 + angle_offset)))); + + return line * radial * edge_intensity * anim_fade * visibility; +} + +void main() +{ + vec2 texcoords = gl_FragCoord.xy / u_screen; + + // Sample the original color + vec4 original = texture(color_buffer, texcoords); + + // Early exit if no speed effect + if (speed_intensity < 0.01) + { + FragColor = original; + return; + } + + // Generate multiple layers of speed lines for depth + float lines = 0.0; + lines += speedLine(texcoords, center, 0.0) * 0.6; + lines += speedLine(texcoords, center, 1.0) * 0.3; + lines += speedLine(texcoords, center, 2.0) * 0.2; + + // Scale by speed intensity + lines *= speed_intensity * 0.8; + + // Clamp lines intensity + lines = clamp(lines, 0.0, 0.6); + + // Base line color (white) + vec3 line_color = vec3(1.0, 1.0, 1.0); + + // Add purple tint for boost (nitro/zipper) + // Purple: RGB(0.7, 0.4, 1.0) + vec3 boost_color = vec3(0.75, 0.5, 1.0); + line_color = mix(line_color, boost_color, boost_intensity * 0.7); + + // Slight glow effect - brighten the line area + vec3 glow = line_color * lines * 1.5; + + // Blend lines with original image (additive blend) + vec3 final_color = original.rgb + glow; + + // Add subtle chromatic aberration near lines for extra punch + if (boost_intensity > 0.3) + { + float aberration = lines * boost_intensity * 0.003; + vec2 dir_to_center = normalize(center - texcoords); + float r = texture(color_buffer, texcoords + dir_to_center * aberration).r; + float b = texture(color_buffer, texcoords - dir_to_center * aberration).b; + final_color.r = mix(final_color.r, r, boost_intensity * 0.3); + final_color.b = mix(final_color.b, b, boost_intensity * 0.3); + } + + FragColor = vec4(final_color, original.a); +} diff --git a/src/graphics/abstract_renderer.hpp b/src/graphics/abstract_renderer.hpp index abb26ce6551..734526d7449 100644 --- a/src/graphics/abstract_renderer.hpp +++ b/src/graphics/abstract_renderer.hpp @@ -80,6 +80,7 @@ class AbstractRenderer virtual void resetPostProcessing() {} virtual void giveBoost(unsigned int cam_index) {} + virtual void setSpeedIntensity(unsigned int cam_index, float speed_intensity, float boost_intensity) {} virtual void removeSkyBox() {} diff --git a/src/graphics/camera/camera.cpp b/src/graphics/camera/camera.cpp index c22946202ff..b49e58d7696 100644 --- a/src/graphics/camera/camera.cpp +++ b/src/graphics/camera/camera.cpp @@ -312,3 +312,38 @@ void Camera::activate(bool alsoActivateInIrrlicht) } } // activate +/** + * Camera Lerp FOV Helper for boost + * use setFOV(). + */ +void Camera::updateDynamicFoV(float dt, float speed_ratio, bool boost_active) +{ + + const float max_fov_increase = 8.0f; + + const float speed_threshold = 0.7f; + + const float threshold_range = 0.3f; + + const float lerp_speed = 4.0f; + + // Factors + float speed_factor = 0.0f; + + if(speed_ratio > speed_threshold) { + speed_factor = + (speed_ratio - speed_threshold) / threshold_range; + } + + float total_factor = speed_factor * (boost_active ? 1.3f : 1.0f); + total_factor = std::min(total_factor, 1.5f); + + float fov_increase = total_factor * DEGREE_TO_RAD * max_fov_increase; + m_target_fov = m_fov + fov_increase; + + float lerp_factor = 1.0f - expf(-lerp_speed * dt); + + m_current_fov = m_current_fov + (m_target_fov - m_current_fov) * lerp_factor; + + m_camera->setFOV(m_current_fov); +} \ No newline at end of file diff --git a/src/graphics/camera/camera.hpp b/src/graphics/camera/camera.hpp index 3182958dd7b..a731b830f45 100644 --- a/src/graphics/camera/camera.hpp +++ b/src/graphics/camera/camera.hpp @@ -122,6 +122,13 @@ class Camera : public NoCopy /** List of all cameras. */ static std::vector m_all_cameras; + // Dynamic FoV variables for speed boost effect + float m_base_fov; + + float m_current_fov; + + float m_target_fov; + protected: /** The camera scene node. */ scene::ICameraSceneNode *m_camera; @@ -247,6 +254,10 @@ class Camera : public NoCopy Vec3 getXYZ() { return Vec3(m_camera->getPosition()); } // ------------------------------------------------------------------------ void setupCamera(); + + void updateDynamicFoV(float dt, float speed_ratio, bool boost_active); + + float getCurrentFoV() const { return m_current_fov; } }; // class Camera #endif diff --git a/src/graphics/camera/camera_normal.cpp b/src/graphics/camera/camera_normal.cpp index a3ac3a369a8..e6e5fa441e8 100644 --- a/src/graphics/camera/camera_normal.cpp +++ b/src/graphics/camera/camera_normal.cpp @@ -20,17 +20,20 @@ #include "graphics/camera/camera_normal.hpp" #include "audio/sfx_manager.hpp" +#include "guiengine/engine.hpp" #include "config/stk_config.hpp" #include "config/user_config.hpp" +#include "graphics/irr_driver.hpp" #include "input/device_manager.hpp" #include "input/input_manager.hpp" #include "input/multitouch_device.hpp" -#include "modes/soccer_world.hpp" #include "karts/kart.hpp" #include "karts/explosion_animation.hpp" #include "karts/kart.hpp" #include "karts/kart_properties.hpp" +#include "karts/max_speed.hpp" #include "karts/skidding.hpp" +#include "modes/soccer_world.hpp" #include "tracks/track.hpp" #include #include @@ -74,9 +77,76 @@ CameraNormal::CameraNormal(Camera::CameraType type, int camera_index, reset(); m_camera->setNearValue(1.0f); + // Initialize speed lines effect variables + m_speed_lines_intensity = 0.0f; + m_speed_lines_boost_intensity = 0.0f; + m_speed_lines_timer = 0.0f; + + // Register as boost observer + MaxSpeed::addBoostObserver(this); + restart(); } // Camera +//----------------------------------------------------------------------------- +/** Destructor - unregisters from boost observer list. + */ +CameraNormal::~CameraNormal() +{ + MaxSpeed::removeBoostObserver(this); +} // ~CameraNormal + +//----------------------------------------------------------------------------- +/** IBoostObserver callback - called when any kart activates a boost. + * Triggers speed lines effect for this camera's kart. + * \param kart The kart that activated the boost. + * \param category The boost category. + * \param add_speed The speed added by the boost. + * \param duration_ticks The duration of the boost in ticks. + */ +void CameraNormal::onBoostActivated(Kart* kart, unsigned int category, + float add_speed, int duration_ticks) +{ +#ifndef SERVER_ONLY + // Only trigger effects for THIS camera's kart (current player) + if (kart != m_kart) return; + if (GUIEngine::isNoGraphics()) return; + + // Effect duration settings + const float EFFECT_DURATION = 2.0f; // Speed lines last 2 seconds + + // Determine intensity based on boost type (strongest takes priority) + float intensity = 0.0f; + switch (category) + { + case MaxSpeed::MS_INCREASE_ZIPPER: + case MaxSpeed::MS_INCREASE_GROUND_ZIPPER: + intensity = 1.0f; // Strongest (zipper) + break; + case MaxSpeed::MS_INCREASE_NITRO: + intensity = 0.8f; // Strong (nitro) + break; + case MaxSpeed::MS_INCREASE_SKIDDING: + case MaxSpeed::MS_INCREASE_RED_SKIDDING: + case MaxSpeed::MS_INCREASE_PURPLE_SKIDDING: + intensity = 0.6f; // Medium (skid bonus) + break; + default: + // Other boost types (slipstream, rubber band, etc.) don't trigger effects + return; + } + + // Only trigger if intensity would be stronger than current effect + if (intensity > m_speed_lines_intensity) + { + // Trigger speed lines effect + m_speed_lines_intensity = intensity; + m_speed_lines_boost_intensity = intensity; + m_speed_lines_timer = EFFECT_DURATION; + } +#endif +} // onBoostActivated + //----------------------------------------------------------------------------- /** Moves the camera smoothly from the current camera position (and target) * to the new position and target. @@ -363,10 +433,62 @@ void CameraNormal::update(float dt) if (!smoothing) { - getCameraSettings(m_last_smooth_mode, &above_kart, &cam_angle, &side_way, + getCameraSettings(m_last_smooth_mode, &above_kart, &cam_angle, &side_way, &distance, &smoothing, &cam_roll_angle); moveCamera(dt, false, cam_angle, distance); } + +#ifndef SERVER_ONLY + // Update speed lines and dynamic FOV effects + // NOTE: Effect TRIGGERING is handled by onBoostActivated() observer callback + // This section only handles decay of existing effects and FOV updates + if (!GUIEngine::isNoGraphics() && m_kart != NULL) + { + const KartProperties *kp = m_kart->getKartProperties(); + float max_speed = kp->getEngineMaxSpeed(); + float current_speed = m_kart->getSpeed(); + float speed_ratio = current_speed / max_speed; + + // Effect duration for decay calculation (must match onBoostActivated) + const float EFFECT_DURATION = 2.0f; + + // Decay speed lines over time + if (m_speed_lines_timer > 0.0f) + { + m_speed_lines_timer -= dt; + + // Smooth decay using remaining timer ratio (ease-out curve) + float decay = std::max(0.0f, m_speed_lines_timer / EFFECT_DURATION); + float display_intensity = m_speed_lines_intensity * decay; + float display_boost = m_speed_lines_boost_intensity * decay; + + irr_driver->setSpeedIntensity(getIndex(), display_intensity, display_boost); + } + else + { + // Effect has ended - reset intensities + m_speed_lines_intensity = 0.0f; + m_speed_lines_boost_intensity = 0.0f; + irr_driver->setSpeedIntensity(getIndex(), 0.0f, 0.0f); + } + + // Update dynamic FOV based on speed and any active boost + // Still need to poll for active boosts (not just activations) for FOV + Kart* kart = dynamic_cast(m_kart); + bool any_boost_active = false; + if (kart) + { + any_boost_active = + kart->getSpeedIncreaseTicksLeft(MaxSpeed::MS_INCREASE_NITRO) > 0 || + kart->getSpeedIncreaseTicksLeft(MaxSpeed::MS_INCREASE_ZIPPER) > 0 || + kart->getSpeedIncreaseTicksLeft(MaxSpeed::MS_INCREASE_GROUND_ZIPPER) > 0 || + kart->getSpeedIncreaseTicksLeft(MaxSpeed::MS_INCREASE_SKIDDING) > 0 || + kart->getSpeedIncreaseTicksLeft(MaxSpeed::MS_INCREASE_RED_SKIDDING) > 0 || + kart->getSpeedIncreaseTicksLeft(MaxSpeed::MS_INCREASE_PURPLE_SKIDDING) > 0; + } + updateDynamicFoV(dt, speed_ratio, any_boost_active); + } +#endif } // update // ---------------------------------------------------------------------------- diff --git a/src/graphics/camera/camera_normal.hpp b/src/graphics/camera/camera_normal.hpp index 43a97af0dff..052f15e86fb 100644 --- a/src/graphics/camera/camera_normal.hpp +++ b/src/graphics/camera/camera_normal.hpp @@ -22,6 +22,7 @@ #define HEADER_CAMERA_NORMAL_HPP #include "graphics/camera/camera.hpp" +#include "karts/boost_observer.hpp" #include "utils/cpp2011.hpp" @@ -29,9 +30,23 @@ * \brief Handles the normal racing camera * \ingroup graphics */ -class CameraNormal : public Camera +class CameraNormal : public Camera, public IBoostObserver { +private: + // ======================================================================== + // Speed lines effect (timer-based, triggered by onBoostActivated) + // ======================================================================== + + /** Current effect intensity (decays over time). */ + float m_speed_lines_intensity = 0.0f; + + /** Boost-specific intensity for color shifting. */ + float m_speed_lines_boost_intensity = 0.0f; + + /** Remaining effect duration. */ + float m_speed_lines_timer = 0.0f; + private: /** Current ambient light for this camera. */ @@ -82,7 +97,7 @@ class CameraNormal : public Camera friend class CameraEnd; CameraNormal(Camera::CameraType type, int camera_index, Kart* kart); - virtual ~CameraNormal() {} + virtual ~CameraNormal(); public: void restart(); @@ -93,6 +108,10 @@ class CameraNormal : public Camera // ------------------------------------------------------------------------ virtual void update(float dt) OVERRIDE; // ------------------------------------------------------------------------ + /** IBoostObserver interface: called when any kart activates a boost. */ + virtual void onBoostActivated(Kart* kart, unsigned int category, + float add_speed, int duration_ticks) OVERRIDE; + // ------------------------------------------------------------------------ /** Sets the ambient light for this camera. */ void setAmbientLight(const video::SColor &color) { m_ambient_light=color; } // ------------------------------------------------------------------------ diff --git a/src/graphics/irr_driver.hpp b/src/graphics/irr_driver.hpp index e332f4f42c7..b4e1532e93d 100644 --- a/src/graphics/irr_driver.hpp +++ b/src/graphics/irr_driver.hpp @@ -325,6 +325,13 @@ class IrrDriver : public IEventReceiver, public NoCopy /** Use motion blur for a short time */ void giveBoost(unsigned int cam_index) { m_renderer->giveBoost(cam_index);} // ------------------------------------------------------------------------ + /** Set speed lines effect intensity for a camera */ + void setSpeedIntensity(unsigned int cam_index, float speed_intensity, + float boost_intensity) + { + m_renderer->setSpeedIntensity(cam_index, speed_intensity, boost_intensity); + } + // ------------------------------------------------------------------------ inline core::vector3df getWind() {return m_wind->getWind();} // ----------------------------------------------------------------------- diff --git a/src/graphics/post_processing.cpp b/src/graphics/post_processing.cpp index 13da073a3c6..b6532bc4b07 100644 --- a/src/graphics/post_processing.cpp +++ b/src/graphics/post_processing.cpp @@ -689,7 +689,7 @@ class MLAAGatherSHader : public TextureShader { public: @@ -706,6 +706,34 @@ class LightningShader : public TextureShader +{ +public: + SpeedLinesShader() + { + loadProgram(OBJECT, GL_VERTEX_SHADER, "screenquad.vert", + GL_FRAGMENT_SHADER, "speed_lines.frag"); + assignUniforms("speed_intensity", "boost_intensity", "time", + "center", "inner_radius"); + assignSamplerNames(0, "color_buffer", ST_BILINEAR_CLAMPED_FILTERED); + } // SpeedLinesShader + // ------------------------------------------------------------------------ + void render(const FrameBuffer &fb, float speed_intensity, + float boost_intensity, float time) + { + setTextureUnits(fb.getRTT()[0]); + // Center point slightly below middle (where kart typically is) + core::vector2df center(0.5f, 0.6f); + // Inner radius where lines start to fade in + float inner_radius = 0.15f; + drawFullScreenEffect(speed_intensity, boost_intensity, time, + center, inner_radius); + } // render +}; // SpeedLinesShader + // ============================================================================ PostProcessing::PostProcessing() @@ -735,10 +763,15 @@ PostProcessing::PostProcessing() */ void PostProcessing::reset() { - m_boost_time.resize(Camera::getNumCameras()); - for (unsigned int i = 0; i < Camera::getNumCameras(); i++) + const unsigned int num_cams = Camera::getNumCameras(); + m_boost_time.resize(num_cams); + m_speed_intensity.resize(num_cams); + m_boost_intensity.resize(num_cams); + for (unsigned int i = 0; i < num_cams; i++) { m_boost_time[i] = 0.0f; + m_speed_intensity[i] = 0.0f; + m_boost_intensity[i] = 0.0f; } // for i isGLSL()) + return; + + if (cam_index < m_speed_intensity.size()) + { + m_speed_intensity[cam_index] = speed_intensity; + m_boost_intensity[cam_index] = boost_intensity; + } +} // setSpeedIntensity + +// ---------------------------------------------------------------------------- +/** Render speed lines effect. + * \param in_fbo Input framebuffer + * \param out_fbo Output framebuffer */ +void PostProcessing::renderSpeedLines(const FrameBuffer &in_fbo, + FrameBuffer &out_fbo) +{ + Camera *cam = Camera::getActiveCamera(); + unsigned int cam_index = cam->getIndex(); + + if (cam_index >= m_speed_intensity.size()) + return; + + float speed_intensity = m_speed_intensity[cam_index]; + float boost_intensity = m_boost_intensity[cam_index]; + + // Skip if no effect needed + if (speed_intensity < 0.01f) + return; + + out_fbo.bind(); + glClear(GL_COLOR_BUFFER_BIT); + + // Get time for animation + float time = 0.0f; + if (World::getWorld()) + time = World::getWorld()->getTime(); + + SpeedLinesShader::getInstance()->render(in_fbo, speed_intensity, + boost_intensity, time); +} // renderSpeedLines + // ---------------------------------------------------------------------------- /** Updates the boost times for all cameras, called once per frame. * \param dt Time step size. @@ -1284,6 +1368,22 @@ FrameBuffer *PostProcessing::render(scene::ICameraSceneNode * const camnode, PROFILER_POP_CPU_MARKER(); } + // Speed lines effect + { + PROFILER_PUSH_CPU_MARKER("- Speed lines", 0xFF, 0x00, 0x00); + Camera *cam = Camera::getActiveCamera(); + unsigned int cam_index = cam ? cam->getIndex() : 0; + if (isRace && cam_index < m_speed_intensity.size() && + m_speed_intensity[cam_index] > 0.01f) + { + FrameBuffer *speed_in = out_fbo; + FrameBuffer *speed_out = in_fbo; + renderSpeedLines(*speed_in, *speed_out); + std::swap(in_fbo, out_fbo); + } + PROFILER_POP_CPU_MARKER(); + } + // Handle lightning rendering { PROFILER_PUSH_CPU_MARKER("- Lightning", 0xFF, 0x00, 0x00); diff --git a/src/graphics/post_processing.hpp b/src/graphics/post_processing.hpp index 264d6540f5c..9bb2395a039 100644 --- a/src/graphics/post_processing.hpp +++ b/src/graphics/post_processing.hpp @@ -46,6 +46,12 @@ class PostProcessing * have a stronger effect. */ std::vector m_boost_time; + /** Speed intensity for speed lines effect [0.0, 1.0] per camera. */ + std::vector m_speed_intensity; + + /** Boost intensity for speed lines color tint [0.0, 1.0] per camera. */ + std::vector m_boost_intensity; + video::ITexture* m_areamap; public: @@ -92,7 +98,17 @@ class PostProcessing /** Use motion blur for a short time */ void giveBoost(unsigned int cam_index); - + + /** Set the speed lines intensity for a camera. + * \param cam_index Camera index + * \param speed_intensity Speed-based intensity [0.0, 1.0] + * \param boost_intensity Boost-based intensity for color tint [0.0, 1.0] */ + void setSpeedIntensity(unsigned int cam_index, float speed_intensity, + float boost_intensity); + + void renderSpeedLines(const FrameBuffer &in_fbo, + FrameBuffer &out_fbo); + /** Render the post-processed scene */ FrameBuffer *render(scene::ICameraSceneNode * const camnode, bool isRace, RTT *rtts); diff --git a/src/graphics/shader_based_renderer.cpp b/src/graphics/shader_based_renderer.cpp index 6c0f0ec8930..904ba89a9be 100644 --- a/src/graphics/shader_based_renderer.cpp +++ b/src/graphics/shader_based_renderer.cpp @@ -660,6 +660,15 @@ void ShaderBasedRenderer::giveBoost(unsigned int cam_index) m_post_processing->giveBoost(cam_index); } +// ---------------------------------------------------------------------------- +void ShaderBasedRenderer::setSpeedIntensity(unsigned int cam_index, + float speed_intensity, + float boost_intensity) +{ + m_post_processing->setSpeedIntensity(cam_index, speed_intensity, + boost_intensity); +} + // ---------------------------------------------------------------------------- void ShaderBasedRenderer::addSkyBox(const std::vector &texture, const std::vector &spherical_harmonics_textures) diff --git a/src/graphics/shader_based_renderer.hpp b/src/graphics/shader_based_renderer.hpp index a3317dbed40..0b9a9ac596f 100644 --- a/src/graphics/shader_based_renderer.hpp +++ b/src/graphics/shader_based_renderer.hpp @@ -94,6 +94,8 @@ class ShaderBasedRenderer: public AbstractRenderer void resetPostProcessing() OVERRIDE; void giveBoost(unsigned int cam_index) OVERRIDE; + void setSpeedIntensity(unsigned int cam_index, float speed_intensity, + float boost_intensity) OVERRIDE; void addSkyBox(const std::vector &texture, const std::vector &spherical_harmonics_textures); diff --git a/src/karts/boost_observer.hpp b/src/karts/boost_observer.hpp new file mode 100644 index 00000000000..b217e4c7bea --- /dev/null +++ b/src/karts/boost_observer.hpp @@ -0,0 +1,56 @@ +// +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2024 SuperTuxKart-Team +// +// 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, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#ifndef HEADER_BOOST_OBSERVER_HPP +#define HEADER_BOOST_OBSERVER_HPP + +class Kart; + +/** + * \brief Observer interface for boost activation events. + * \ingroup karts + * + * Classes implementing this interface can register with MaxSpeed to receive + * notifications when a boost activates. This enables event-driven visual effects + * instead of per-frame polling. + * + * Usage: + * - KartGFX: Receives events for ALL karts to trigger particle bursts + * - CameraNormal: Receives events but only acts on the camera's own kart + * to trigger speed lines shader and camera pull-back + */ +class IBoostObserver +{ +public: + virtual ~IBoostObserver() = default; + + /** Called when a boost activates (NOT every frame - only on activation). + * Implementers should check if kart matches their target before acting. + * + * @param kart The kart that activated the boost + * @param category The boost type (MS_INCREASE_NITRO, MS_INCREASE_ZIPPER, etc.) + * @param add_speed The speed increase value + * @param duration_ticks How long the boost lasts in game ticks + */ + virtual void onBoostActivated(Kart* kart, + unsigned int category, + float add_speed, + int duration_ticks) = 0; +}; + +#endif // HEADER_BOOST_OBSERVER_HPP diff --git a/src/karts/max_speed.cpp b/src/karts/max_speed.cpp index 04b1d9fc7a0..92e4b5f455f 100644 --- a/src/karts/max_speed.cpp +++ b/src/karts/max_speed.cpp @@ -19,6 +19,7 @@ #include "karts/max_speed.hpp" #include "config/stk_config.hpp" +#include "karts/boost_observer.hpp" #include "karts/kart.hpp" #include "karts/kart_properties.hpp" #include "network/network_string.hpp" @@ -28,6 +29,9 @@ #include #include +// Static observer registry +std::vector MaxSpeed::s_boost_observers; + /** This class handles maximum speed for karts. Several factors can influence * the maximum speed a kart can drive, some will decrease the maximum speed, * some will increase the maximum speed. @@ -57,6 +61,10 @@ MaxSpeed::MaxSpeed(Kart *kart) // This can be used if command line option -N is used m_current_max_speed = 0; m_last_triggered_skid_level = 0; + + // Initialize boost activation tracking for edge detection + for (unsigned int i = 0; i < MS_INCREASE_MAX; i++) + m_prev_active[i] = false; } // MaxSpeed // ---------------------------------------------------------------------------- @@ -84,9 +92,63 @@ void MaxSpeed::reset(bool leave_squash) { SpeedIncrease si; m_speed_increase[i] = si; + m_prev_active[i] = false; // Reset activation tracking } } // reset +// ============================================================================ +// Boost Observer Pattern Implementation +// ============================================================================ + +// ---------------------------------------------------------------------------- +/** Register an observer to receive boost activation events from all karts. */ +void MaxSpeed::addBoostObserver(IBoostObserver* observer) +{ + if (observer == nullptr) return; + // Avoid duplicate registration + for (auto* obs : s_boost_observers) + { + if (obs == observer) return; + } + s_boost_observers.push_back(observer); +} // addBoostObserver + +// ---------------------------------------------------------------------------- +/** Unregister an observer. Call this before destroying the observer. */ +void MaxSpeed::removeBoostObserver(IBoostObserver* observer) +{ + auto it = std::find(s_boost_observers.begin(), s_boost_observers.end(), observer); + if (it != s_boost_observers.end()) + { + s_boost_observers.erase(it); + } +} // removeBoostObserver + +// ---------------------------------------------------------------------------- +/** Clear all observers. Called during cleanup. */ +void MaxSpeed::clearBoostObservers() +{ + s_boost_observers.clear(); +} // clearBoostObservers + +// ---------------------------------------------------------------------------- +/** Notifies all registered observers of a boost activation. + * Called on rising edge (inactive -> active) only. + * \param category The boost category (MS_INCREASE_*) + * \param add_speed The speed increase value + * \param duration How long the boost lasts in ticks + */ +void MaxSpeed::notifyBoostActivation(unsigned int category, float add_speed, int duration) +{ + for (auto* observer : s_boost_observers) + { + if (observer != nullptr) + { + observer->onBoostActivated(m_kart, category, add_speed, duration); + } + } +} // notifyBoostActivation + // ---------------------------------------------------------------------------- /** Sets an increased maximum speed for a category. * \param category The category for which to set the higher maximum speed. @@ -110,6 +172,11 @@ void MaxSpeed::increaseMaxSpeed(unsigned int category, float add_speed, return; } + // Check if this is a NEW activation (rising edge detection) + // A boost is considered "new" if it wasn't previously tracked as active + bool was_active = m_prev_active[category]; + bool is_activation = !was_active && add_speed > 0.0f; + if (category == MS_INCREASE_SKIDDING) m_last_triggered_skid_level = 1; else if (category == MS_INCREASE_RED_SKIDDING) @@ -158,6 +225,14 @@ void MaxSpeed::increaseMaxSpeed(unsigned int category, float add_speed, m_speed_increase[category].m_fade_out_time = fade; m_speed_increase[category].m_current_speedup = add_speed; m_speed_increase[category].m_engine_force = (uint16_t)engine_force_i; + + // Update activation tracking and notify observers on rising edge + m_prev_active[category] = (add_speed > 0.0f); + + if (is_activation) + { + notifyBoostActivation(category, add_speed, duration); + } } // increaseMaxSpeed // ---------------------------------------------------------------------------- @@ -447,6 +522,13 @@ void MaxSpeed::update(int ticks) speedup.update(ticks); m_current_max_speed += speedup.getSpeedIncrease(); m_add_engine_force += speedup.getEngineForce(); + + // Reset activation tracking when boost naturally expires + // This allows the next activation to be detected as a rising edge + if (m_prev_active[i] && !speedup.isActive()) + { + m_prev_active[i] = false; + } } // Pick the highest applicable speed boost and the highest applicable engine boost, diff --git a/src/karts/max_speed.hpp b/src/karts/max_speed.hpp index 77e32e2537e..2b120e6762e 100644 --- a/src/karts/max_speed.hpp +++ b/src/karts/max_speed.hpp @@ -21,15 +21,45 @@ #include "utils/types.hpp" #include +#include /** \defgroup karts */ class Kart; class BareNetworkString; +class IBoostObserver; class MaxSpeed { friend class KartRewinder; + +private: + // ======================================================================== + // Boost Observer Pattern - Global observer registry + // ======================================================================== + /** Global list of boost observers. All observers receive events from any kart. */ + static std::vector s_boost_observers; + + /** Tracks previous frame activation state for edge detection (per category). */ + bool m_prev_active[10]; // Sized for MS_INCREASE_MAX categories + + /** Notifies all registered observers of a boost activation. + * Called on rising edge (inactive -> active) only. */ + void notifyBoostActivation(unsigned int category, float add_speed, int duration); + +public: + // ======================================================================== + // Boost Observer Registration (static - applies to all MaxSpeed instances) + // ======================================================================== + /** Register an observer to receive boost activation events from all karts. */ + static void addBoostObserver(IBoostObserver* observer); + + /** Unregister an observer. Call this before destroying the observer. */ + static void removeBoostObserver(IBoostObserver* observer); + + /** Clear all observers. Called during cleanup. */ + static void clearBoostObservers(); + public: /** The categories to use for increasing the speed of a kart: * Increase due to zipper, slipstream, nitro, rubber band,