use glam::{Vec2, Vec3};
pub const MAX_SHOCKWAVES: usize = 12;
#[derive(Clone, Copy, Debug)]
pub struct Shockwave {
pub origin: Vec2,
pub age: f32,
pub duration: f32,
pub speed: f32,
pub width: f32,
pub strength: f32,
}
impl Shockwave {
pub fn radius(&self) -> f32 {
self.age * self.speed
}
pub fn current_strength(&self) -> f32 {
let t = (self.age / self.duration.max(1e-3)).clamp(0.0, 1.0);
let fade = 1.0 - t;
self.strength * fade * fade
}
pub fn is_dead(&self) -> bool {
self.age >= self.duration
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Reflection {
pub y: f32,
pub strength: f32,
pub fade: f32,
pub ripple: f32,
pub blur: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScreenLight {
pub x: f32,
pub y: f32,
pub radius: f32,
pub color: Vec3,
pub intensity: f32,
pub shadows: bool,
}
pub const MAX_LIGHTS: usize = 32;
#[derive(Clone, Debug)]
pub struct ScreenFx {
pub lights: Vec<ScreenLight>,
pub ambient: Vec3,
pub shadow_density: f32,
pub shockwaves: Vec<Shockwave>,
pub reflection: Option<Reflection>,
pub haze: f32,
pub auto_shafts: bool,
pub flash: Vec3,
pub flash_decay: f32,
pub shaft_origin: Option<Vec2>,
pub shaft_strength: f32,
pub shaft_target: f32,
pub shaft_ease: f32,
pub shaft_tint: Vec3,
pub time: f32,
}
impl Default for ScreenFx {
fn default() -> Self {
Self {
shockwaves: Vec::with_capacity(MAX_SHOCKWAVES),
lights: Vec::with_capacity(MAX_LIGHTS),
ambient: Vec3::ONE,
shadow_density: 5.0,
reflection: None,
haze: 0.0,
auto_shafts: false,
flash: Vec3::ZERO,
flash_decay: 0.0005,
shaft_origin: None,
shaft_strength: 0.0,
shaft_target: 0.0,
shaft_ease: 3.0,
shaft_tint: Vec3::ONE,
time: 0.0,
}
}
}
impl ScreenFx {
pub fn new() -> Self {
Self::default()
}
pub fn shockwave(&mut self, x: f32, y: f32, strength: f32) {
self.shockwave_with(Shockwave {
origin: Vec2::new(x, y),
age: 0.0,
duration: 0.55,
speed: 1400.0,
width: 48.0,
strength,
});
}
pub fn shockwave_with(&mut self, wave: Shockwave) {
if self.shockwaves.len() >= MAX_SHOCKWAVES {
self.shockwaves.remove(0);
}
self.shockwaves.push(wave);
}
pub fn flash(&mut self, color: Vec3, strength: f32) {
let add = color * strength;
self.flash = Vec3::new(
self.flash.x.max(add.x),
self.flash.y.max(add.y),
self.flash.z.max(add.z),
);
}
pub fn light_shaft_at(&mut self, x: f32, y: f32, strength: f32) {
self.shaft_origin = Some(Vec2::new(x, y));
self.shaft_target = strength.max(0.0);
}
pub fn clear_light_shaft(&mut self) {
self.shaft_target = 0.0;
}
pub fn light(&mut self, x: f32, y: f32, radius: f32, color: Vec3, intensity: f32) {
self.push_light(ScreenLight { x, y, radius, color, intensity, shadows: true });
}
pub fn light_unshadowed(&mut self, x: f32, y: f32, radius: f32, color: Vec3, intensity: f32) {
self.push_light(ScreenLight { x, y, radius, color, intensity, shadows: false });
}
pub fn push_light(&mut self, light: ScreenLight) {
if self.lights.len() < MAX_LIGHTS && light.intensity > 0.0 && light.radius > 0.0 {
self.lights.push(light);
}
}
pub fn lighting_active(&self) -> bool {
!self.lights.is_empty() || self.ambient != Vec3::ONE
}
pub fn pack_lights(&self, screen_w: f32, screen_h: f32) -> (Vec<f32>, Vec<f32>, usize) {
let w = screen_w.max(1.0);
let h = screen_h.max(1.0);
let mut pos = Vec::with_capacity(MAX_LIGHTS * 4);
let mut col = Vec::with_capacity(MAX_LIGHTS * 4);
let mut n = 0;
for l in self.lights.iter().take(MAX_LIGHTS) {
pos.extend_from_slice(&[l.x / w, 1.0 - l.y / h, l.radius, if l.shadows { 1.0 } else { 0.0 }]);
col.extend_from_slice(&[l.color.x, l.color.y, l.color.z, l.intensity]);
n += 1;
}
(pos, col, n)
}
pub fn reflect_at(&mut self, y: f32, strength: f32, fade: f32) {
self.reflection = Some(Reflection {
y,
strength: strength.max(0.0),
fade: fade.max(1.0),
ripple: 0.0,
blur: 1.5,
});
}
pub fn clear_reflection(&mut self) {
self.reflection = None;
}
pub fn pack_reflection(&self, screen_w: f32, screen_h: f32) -> ([f32; 4], f32) {
let _ = screen_w;
match self.reflection {
Some(r) if r.strength > 0.0 => {
let h = screen_h.max(1.0);
([1.0 - r.y / h, r.strength, r.fade / h, r.ripple], r.blur)
}
_ => ([0.0, 0.0, 1.0, 0.0], 0.0),
}
}
pub fn tick(&mut self, dt: f32) {
self.time += dt;
for w in &mut self.shockwaves {
w.age += dt;
}
self.shockwaves.retain(|w| !w.is_dead());
let keep = self.flash_decay.max(1e-6).powf(dt);
self.flash *= keep;
if self.flash.max_element() < 0.002 {
self.flash = Vec3::ZERO;
}
let ease = (self.shaft_ease * dt).clamp(0.0, 1.0);
self.shaft_strength += (self.shaft_target - self.shaft_strength) * ease;
if self.shaft_target <= 0.0 && self.shaft_strength < 0.005 {
self.shaft_strength = 0.0;
self.shaft_origin = None;
}
}
pub fn is_idle(&self) -> bool {
self.shockwaves.is_empty()
&& self.flash == Vec3::ZERO
&& self.shaft_strength <= 0.0
&& self.reflection.is_none()
&& self.haze <= 0.0
&& !self.lighting_active()
}
pub fn pack_shockwaves(&self, screen_w: f32, screen_h: f32)
-> ([[f32; 4]; MAX_SHOCKWAVES], [f32; MAX_SHOCKWAVES], usize)
{
let mut data = [[0.0f32; 4]; MAX_SHOCKWAVES];
let mut strength = [0.0f32; MAX_SHOCKWAVES];
let w = screen_w.max(1.0);
let h = screen_h.max(1.0);
let mut n = 0;
for wave in self.shockwaves.iter().take(MAX_SHOCKWAVES) {
let s = wave.current_strength();
if s <= 0.01 {
continue;
}
data[n] = [
wave.origin.x / w,
1.0 - wave.origin.y / h,
wave.radius(),
wave.width.max(1.0),
];
strength[n] = s;
n += 1;
}
(data, strength, n)
}
pub fn pack_shaft(&self, screen_w: f32, screen_h: f32) -> [f32; 3] {
match self.shaft_origin {
Some(o) if self.shaft_strength > 0.0 => [
o.x / screen_w.max(1.0),
1.0 - o.y / screen_h.max(1.0),
self.shaft_strength,
],
_ => [0.5, 0.5, 0.0],
}
}
}