use crate::ecs::generational_registry::registry_entry_by_name_mut;
use crate::ecs::lines::components::Line;
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::vfx::components::{Beam, LightningBolt, Trail};
use crate::ecs::world::commands::{EcsCommand, queue_ecs_command};
use crate::ecs::world::{
BEAM, LIGHTNING_BOLT, LINES, LOCAL_TRANSFORM, MATERIAL_REF, TRAIL, VFX_ANIMATOR, World,
};
use freecs::Entity;
use nalgebra_glm::{Vec3, vec4};
pub fn update_vfx_system(world: &mut World) {
let delta_time = world.resources.window.timing.delta_time;
update_beams(world, delta_time);
update_lightning(world, delta_time);
update_trails(world, delta_time);
update_animators(world, delta_time);
}
fn perpendicular_basis(direction: Vec3) -> (Vec3, Vec3) {
let reference = if direction.y.abs() < 0.95 {
Vec3::new(0.0, 1.0, 0.0)
} else {
Vec3::new(1.0, 0.0, 0.0)
};
let first = direction.cross(&reference).normalize();
let second = direction.cross(&first).normalize();
(first, second)
}
fn update_beams(world: &mut World, delta_time: f32) {
let entities: Vec<Entity> = world.core.query_entities(BEAM | LINES).collect();
for entity in entities {
let Some(mut beam) = world.core.get_beam(entity).copied() else {
continue;
};
beam.age += delta_time;
let segments = build_beam_lines(&beam);
if let Some(lines) = world.core.get_lines_mut(entity) {
lines.lines = segments;
lines.mark_dirty();
}
if let Some(stored) = world.core.get_beam_mut(entity) {
*stored = beam;
}
}
}
fn build_beam_lines(beam: &Beam) -> Vec<Line> {
let axis = beam.end - beam.start;
let length = axis.magnitude();
if length <= f32::EPSILON {
return Vec::new();
}
let direction = axis / length;
let (first, second) = perpendicular_basis(direction);
let flicker = 1.0 - beam.flicker * (0.5 - 0.5 * (beam.age * beam.flicker_speed).cos());
let core = beam.color * beam.intensity * flicker;
let strands = beam.strands.max(1);
let mut lines = Vec::with_capacity(strands as usize);
for strand in 0..strands {
let (offset, falloff) = if strand == 0 {
(Vec3::zeros(), 1.0)
} else {
let angle =
(strand as f32 - 1.0) / (strands as f32 - 1.0).max(1.0) * std::f32::consts::TAU;
let offset = (first * angle.cos() + second * angle.sin()) * beam.width;
(offset, 0.45)
};
lines.push(Line {
start: beam.start + offset,
end: beam.end + offset,
color: vec4(core.x, core.y, core.z, beam.alpha * falloff),
});
}
lines
}
fn update_lightning(world: &mut World, delta_time: f32) {
let entities: Vec<Entity> = world.core.query_entities(LIGHTNING_BOLT | LINES).collect();
for entity in entities {
let Some(mut bolt) = world.core.get_lightning_bolt(entity).copied() else {
continue;
};
bolt.timer += delta_time;
let regenerate = bolt.timer >= bolt.regen_interval;
if regenerate {
bolt.timer = 0.0;
bolt.seed = bolt.seed.wrapping_mul(1664525).wrapping_add(1013904223);
let segments = build_lightning_lines(&bolt);
if let Some(lines) = world.core.get_lines_mut(entity) {
lines.lines = segments;
lines.mark_dirty();
}
}
if let Some(stored) = world.core.get_lightning_bolt_mut(entity) {
*stored = bolt;
}
}
}
fn next_random(state: &mut u32) -> f32 {
*state = state.wrapping_mul(1664525).wrapping_add(1013904223);
(*state >> 8) as f32 / 16_777_216.0
}
fn build_lightning_lines(bolt: &LightningBolt) -> Vec<Line> {
let axis = bolt.end - bolt.start;
let length = axis.magnitude();
if length <= f32::EPSILON {
return Vec::new();
}
let direction = axis / length;
let (first, second) = perpendicular_basis(direction);
let color = bolt.color * bolt.intensity;
let line_color = vec4(color.x, color.y, color.z, bolt.alpha);
let mut state = bolt.seed | 1;
let segments = bolt.segments.max(2);
let mut path = Vec::with_capacity(segments as usize + 1);
for index in 0..=segments {
let fraction = index as f32 / segments as f32;
let base = bolt.start + axis * fraction;
let taper = (fraction * (1.0 - fraction) * 4.0).clamp(0.0, 1.0);
let lateral = if index == 0 || index == segments {
Vec3::zeros()
} else {
let displace_first = (next_random(&mut state) - 0.5) * 2.0;
let displace_second = (next_random(&mut state) - 0.5) * 2.0;
(first * displace_first + second * displace_second) * bolt.jaggedness * taper
};
path.push(base + lateral);
}
let mut lines = Vec::new();
for window in path.windows(2) {
lines.push(Line {
start: window[0],
end: window[1],
color: line_color,
});
}
for _ in 0..bolt.branch_count {
let anchor = 1 + (next_random(&mut state) * (segments as f32 - 2.0)) as usize;
let anchor = anchor.min(path.len().saturating_sub(2));
let origin = path[anchor];
let branch_direction = (direction
+ first * (next_random(&mut state) - 0.5) * 2.0 * bolt.branch_spread
+ second * (next_random(&mut state) - 0.5) * 2.0 * bolt.branch_spread)
.normalize();
let branch_length = length * 0.25 * (0.5 + next_random(&mut state));
let branch_segments = 4u32;
let mut previous = origin;
for index in 1..=branch_segments {
let fraction = index as f32 / branch_segments as f32;
let jitter = (first * (next_random(&mut state) - 0.5)
+ second * (next_random(&mut state) - 0.5))
* bolt.jaggedness
* 0.6;
let next = origin + branch_direction * branch_length * fraction + jitter;
let fade = bolt.alpha * (1.0 - fraction) * 0.7;
lines.push(Line {
start: previous,
end: next,
color: vec4(color.x, color.y, color.z, fade),
});
previous = next;
}
}
lines
}
fn update_trails(world: &mut World, delta_time: f32) {
let entities: Vec<Entity> = world.core.query_entities(TRAIL | LINES).collect();
for entity in entities {
let segments = {
let Some(trail) = world.core.get_trail_mut(entity) else {
continue;
};
trail.age += delta_time;
let angle = trail.age * trail.speed;
let (first, second) = perpendicular_basis(trail.axis.normalize());
let position =
trail.center + (first * angle.cos() + second * angle.sin()) * trail.radius;
trail.points.push(position);
let max_points = trail.max_points.max(2) as usize;
while trail.points.len() > max_points {
trail.points.remove(0);
}
build_trail_lines(trail)
};
if let Some(lines) = world.core.get_lines_mut(entity) {
lines.lines = segments;
lines.mark_dirty();
}
}
}
fn build_trail_lines(trail: &Trail) -> Vec<Line> {
if trail.points.len() < 2 {
return Vec::new();
}
let color = trail.color * trail.intensity;
let count = trail.points.len();
let mut lines = Vec::with_capacity(count - 1);
for index in 0..count - 1 {
let head_fraction = (index + 1) as f32 / count as f32;
let alpha = trail.alpha * head_fraction;
lines.push(Line {
start: trail.points[index],
end: trail.points[index + 1],
color: vec4(color.x, color.y, color.z, alpha),
});
}
lines
}
fn update_animators(world: &mut World, delta_time: f32) {
let entities: Vec<Entity> = world
.core
.query_entities(VFX_ANIMATOR | LOCAL_TRANSFORM | MATERIAL_REF)
.collect();
let mut to_despawn = Vec::new();
for entity in entities {
let Some(mut animator) = world.core.get_vfx_animator(entity).copied() else {
continue;
};
animator.age += delta_time;
let progress = if animator.lifetime > 0.0 {
(animator.age / animator.lifetime).clamp(0.0, 1.0)
} else {
0.0
};
let scale = animator.start_scale + (animator.end_scale - animator.start_scale) * progress;
let pulse = 1.0
+ animator.pulse_amplitude
* (animator.age * animator.pulse_frequency * std::f32::consts::TAU).sin();
let fade = if animator.fade_alpha {
1.0 - progress
} else {
1.0
};
let alpha = (animator.base_alpha * fade * pulse).clamp(0.0, 1.0);
let orbit_position = if animator.orbit_radius > 0.0 {
let (first, second) = perpendicular_basis(animator.orbit_axis.normalize());
let angle = animator.age * animator.orbit_speed;
Some(
animator.orbit_center
+ (first * angle.cos() + second * angle.sin()) * animator.orbit_radius,
)
} else {
None
};
if let Some(transform) = world.core.get_local_transform_mut(entity) {
transform.scale = scale;
if let Some(position) = orbit_position {
transform.translation = position;
}
if animator.spin_speed != 0.0 {
transform.rotation = nalgebra_glm::quat_angle_axis(
animator.age * animator.spin_speed,
&animator.spin_axis.normalize(),
);
}
}
mark_local_transform_dirty(world, entity);
let material_name = world
.core
.get_material_ref(entity)
.map(|material_ref| material_ref.name.clone());
if let Some(name) = material_name {
if let Some(material) = registry_entry_by_name_mut(
&mut world.resources.assets.material_registry.registry,
&name,
) {
let color = animator.peak_color;
material.base_color = [color.x, color.y, color.z, alpha];
material.emissive_factor = [color.x, color.y, color.z];
material.emissive_strength = animator.emissive_strength;
}
world
.resources
.mesh_render_state
.mark_material_dirty(entity);
}
if let Some(stored) = world.core.get_vfx_animator_mut(entity) {
*stored = animator;
}
if animator.lifetime > 0.0 && animator.age >= animator.lifetime && animator.despawn_on_end {
to_despawn.push(entity);
}
}
for entity in to_despawn {
queue_ecs_command(world, EcsCommand::DespawnRecursive { entity });
}
}