game_dev_tools/
game_dev_tools.rs

1#![allow(dead_code, clippy::missing_const_for_fn, unused_imports)]
2
3// =============================================================================
4// GAME DEV TOOLS - Reusable Bevy Framework
5// =============================================================================
6
7use bevy::anti_alias::fxaa::Fxaa;
8use bevy::audio::{AudioSource, PlaybackMode, Volume};
9use bevy::camera::Exposure;
10use bevy::core_pipeline::tonemapping::{DebandDither, Tonemapping};
11use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
12use bevy::mesh::MeshMergeError;
13use bevy::post_process::bloom::{Bloom, BloomCompositeMode};
14use bevy::prelude::*;
15use bevy::render::view::Hdr;
16use bevy::window::{CursorGrabMode, MonitorSelection, PrimaryWindow, WindowMode};
17use bevy_rapier2d::prelude::*;
18use std::time::Duration;
19
20// =============================================================================
21// CONFIGURATION - Make everything configurable
22// =============================================================================
23
24#[derive(Resource, Clone)]
25pub struct GameDevConfig {
26    pub window_width: f32,
27    pub window_height: f32,
28    pub max_projectile_distance: f32,
29    pub max_entity_distance: f32,
30    pub max_particle_distance: f32,
31    pub particle_counts: ParticleConfig,
32}
33
34impl Default for GameDevConfig {
35    fn default() -> Self {
36        Self {
37            window_width: 3840.0,
38            window_height: 2160.0,
39            max_projectile_distance: 2000.0,
40            max_entity_distance: 1000.0,
41            max_particle_distance: 800.0,
42            particle_counts: ParticleConfig::default(),
43        }
44    }
45}
46
47#[derive(Clone)]
48pub struct ParticleConfig {
49    pub explosion_sparks: usize,
50    pub muzzle_flash: usize,
51    pub starfield: usize,
52}
53
54impl Default for ParticleConfig {
55    fn default() -> Self {
56        Self {
57            explosion_sparks: 8,
58            muzzle_flash: 5,
59            starfield: 300,
60        }
61    }
62}
63
64// =============================================================================
65// GENERIC COMPONENTS - Reusable across games
66// =============================================================================
67
68#[derive(Component)]
69pub struct Health {
70    pub current: f32,
71    pub max: f32,
72}
73
74impl Health {
75    pub fn new(health: f32) -> Self {
76        Self {
77            current: health,
78            max: health,
79        }
80    }
81
82    pub fn take_damage(&mut self, damage: f32) -> bool {
83        self.current = (self.current - damage).max(0.0);
84        self.current <= 0.0
85    }
86
87    pub fn heal(&mut self, amount: f32) {
88        self.current = (self.current + amount).min(self.max);
89    }
90
91    pub fn is_alive(&self) -> bool {
92        self.current > 0.0
93    }
94
95    pub fn percentage(&self) -> f32 {
96        if self.max > 0.0 {
97            self.current / self.max
98        } else {
99            0.0
100        }
101    }
102}
103
104#[derive(Component)]
105pub struct Lives {
106    pub current: i32,
107    pub max: i32,
108}
109
110impl Lives {
111    pub fn new(lives: i32) -> Self {
112        Self {
113            current: lives,
114            max: lives,
115        }
116    }
117
118    pub fn take_damage(&mut self) -> bool {
119        self.current -= 1;
120        self.current <= 0
121    }
122
123    pub fn is_alive(&self) -> bool {
124        self.current > 0
125    }
126}
127
128#[derive(Component)]
129pub struct Velocity(pub Vec2);
130
131#[derive(Component)]
132pub struct SpacePhysics {
133    pub velocity: Vec2,
134    pub acceleration: Vec2,
135    pub max_thrust: f32,
136    pub drag_coefficient: f32,
137    pub rotation_velocity: f32,
138    pub rotation_drag: f32,
139    pub max_speed: f32,
140}
141
142impl Default for SpacePhysics {
143    fn default() -> Self {
144        Self {
145            velocity: Vec2::ZERO,
146            acceleration: Vec2::ZERO,
147            max_thrust: 400.0,
148            drag_coefficient: 0.98,
149            rotation_velocity: 0.0,
150            rotation_drag: 0.95,
151            max_speed: 600.0,
152        }
153    }
154}
155
156#[derive(Component)]
157pub struct Projectile {
158    pub damage: f32,
159    pub lifetime: Option<Timer>,
160}
161
162impl Projectile {
163    pub fn new(damage: f32) -> Self {
164        Self {
165            damage,
166            lifetime: None,
167        }
168    }
169
170    pub fn with_lifetime(damage: f32, seconds: f32) -> Self {
171        Self {
172            damage,
173            lifetime: Some(Timer::from_seconds(seconds, TimerMode::Once)),
174        }
175    }
176}
177
178#[derive(Component)]
179pub struct Destructible {
180    pub health: f32,
181    pub max_health: f32,
182    pub on_death: Option<DestructionBehavior>,
183}
184
185#[derive(Clone)]
186pub enum DestructionBehavior {
187    Split { count: u32, size_multiplier: f32 },
188    Explode { intensity: f32 },
189    Nothing,
190}
191
192// =============================================================================
193// CAMERA SYSTEM - Screen Shake & Effects
194// =============================================================================
195
196#[derive(Component)]
197pub struct CinematicCamera {
198    pub bloom_intensity: f32,
199    pub exposure: f32,
200    pub base_position: Vec3,
201    pub target_zoom: f32,
202    pub current_zoom: f32,
203}
204
205impl Default for CinematicCamera {
206    fn default() -> Self {
207        Self {
208            bloom_intensity: 0.3,
209            exposure: 0.0,
210            base_position: Vec3::new(0.0, 0.0, 1000.0),
211            target_zoom: 1.0,
212            current_zoom: 1.0,
213        }
214    }
215}
216
217#[derive(Resource, Default, Clone)]
218pub struct ScreenShake {
219    pub intensity: f32,
220    pub duration: f32,
221    pub timer: f32,
222    pub enabled: bool,
223}
224
225impl ScreenShake {
226    pub fn trigger(&mut self, intensity: f32, duration: f32) {
227        self.intensity = intensity;
228        self.duration = duration;
229        self.timer = 0.0;
230        self.enabled = true;
231    }
232
233    pub fn is_active(&self) -> bool {
234        self.enabled && self.timer < self.duration
235    }
236
237    pub fn get_shake_offset(&self) -> Vec3 {
238        if !self.is_active() {
239            return Vec3::ZERO;
240        }
241
242        let progress = (self.timer / self.duration).clamp(0.0, 1.0);
243        let fade = (1.0 - progress).powi(2);
244        let current_intensity = self.intensity * fade;
245
246        let time_seed = (self.timer * 50.0) as u64;
247        let shake_x = (fastrand::Rng::with_seed(time_seed).f32() - 0.5) * 2.0 * current_intensity;
248        let shake_y =
249            (fastrand::Rng::with_seed(time_seed + 1).f32() - 0.5) * 2.0 * current_intensity;
250
251        Vec3::new(shake_x, shake_y, 0.0)
252    }
253}
254
255pub fn unified_camera_shake_system(
256    mut camera_query: Query<(&mut Transform, &mut CinematicCamera), With<Camera>>,
257    mut screen_shake: ResMut<ScreenShake>,
258    time: Res<Time>,
259) {
260    if screen_shake.is_active() {
261        screen_shake.timer += time.delta_secs();
262        if screen_shake.timer >= screen_shake.duration {
263            screen_shake.enabled = false;
264        }
265    }
266
267    for (mut transform, cinematic) in camera_query.iter_mut() {
268        if screen_shake.is_active() {
269            let shake_offset = screen_shake.get_shake_offset();
270            transform.translation = cinematic.base_position + shake_offset;
271        } else {
272            transform.translation = cinematic.base_position;
273        }
274    }
275}
276
277// =============================================================================
278// PARTICLE SYSTEM - Generic Particle Management
279// =============================================================================
280
281#[derive(Component)]
282pub struct Particle {
283    pub lifetime: Timer,
284    pub start_color: Color,
285    pub end_color: Color,
286    pub start_size: f32,
287    pub end_size: f32,
288    pub fade_over_time: bool,
289}
290
291impl Particle {
292    pub fn new(
293        lifetime_secs: f32,
294        start_color: Color,
295        end_color: Color,
296        start_size: f32,
297        end_size: f32,
298    ) -> Self {
299        Self {
300            lifetime: Timer::from_seconds(lifetime_secs, TimerMode::Once),
301            start_color,
302            end_color,
303            start_size,
304            end_size,
305            fade_over_time: true,
306        }
307    }
308
309    pub fn explosion_spark() -> Self {
310        Self::new(
311            0.8,
312            Color::srgb(1.0, 1.0, 0.8),
313            Color::srgb(1.0, 0.3, 0.0),
314            4.0,
315            1.0,
316        )
317    }
318
319    pub fn smoke() -> Self {
320        Self::new(
321            1.5,
322            Color::srgba(0.4, 0.3, 0.2, 0.8),
323            Color::srgba(0.2, 0.2, 0.2, 0.0),
324            8.0,
325            20.0,
326        )
327    }
328}
329
330#[derive(Component)]
331pub struct ParticleBehavior {
332    pub gravity: Vec2,
333    pub drag: f32,
334    pub spin_speed: f32,
335}
336
337impl ParticleBehavior {
338    pub fn explosion() -> Self {
339        Self {
340            gravity: Vec2::new(0.0, -50.0),
341            drag: 0.98,
342            spin_speed: (fastrand::f32() - 0.5) * 10.0,
343        }
344    }
345
346    pub fn floating() -> Self {
347        Self {
348            gravity: Vec2::new(0.0, -20.0),
349            drag: 0.99,
350            spin_speed: (fastrand::f32() - 0.5) * 5.0,
351        }
352    }
353}
354
355pub fn update_particles(
356    mut commands: Commands,
357    mut particle_query: Query<(
358        Entity,
359        &mut Sprite,
360        &mut Transform,
361        &mut Particle,
362        &ParticleBehavior,
363        &mut Velocity,
364    )>,
365    time: Res<Time>,
366) {
367    for (entity, mut sprite, mut transform, mut particle, behavior, mut velocity) in
368        particle_query.iter_mut()
369    {
370        particle.lifetime.tick(time.delta());
371
372        if particle.lifetime.finished() {
373            commands.entity(entity).despawn();
374            continue;
375        }
376
377        let t = particle.lifetime.elapsed_secs() / particle.lifetime.duration().as_secs_f32();
378
379        if particle.fade_over_time {
380            sprite.color = Color::srgba(
381                particle.start_color.to_srgba().red * (1.0 - t)
382                    + particle.end_color.to_srgba().red * t,
383                particle.start_color.to_srgba().green * (1.0 - t)
384                    + particle.end_color.to_srgba().green * t,
385                particle.start_color.to_srgba().blue * (1.0 - t)
386                    + particle.end_color.to_srgba().blue * t,
387                particle.start_color.to_srgba().alpha * (1.0 - t)
388                    + particle.end_color.to_srgba().alpha * t,
389            );
390        }
391
392        let current_size = particle.start_size * (1.0 - t) + particle.end_size * t;
393        sprite.custom_size = Some(Vec2::splat(current_size));
394
395        velocity.0 += behavior.gravity * time.delta_secs();
396        velocity.0 *= behavior.drag;
397
398        transform.rotation *= Quat::from_rotation_z(behavior.spin_speed * time.delta_secs());
399    }
400}
401
402pub fn spawn_explosion_particles(
403    commands: &mut Commands,
404    position: Vec2,
405    count: usize,
406    size_multiplier: f32,
407) {
408    for _ in 0..count {
409        let angle = fastrand::f32() * 2.0 * std::f32::consts::PI;
410        let speed = 100.0 + fastrand::f32() * 200.0 * size_multiplier;
411        let velocity = Vec2::new(angle.cos(), angle.sin()) * speed;
412
413        commands.spawn((
414            Sprite {
415                color: Color::srgb(1.0, 1.0, 0.8),
416                custom_size: Some(Vec2::splat(4.0 * size_multiplier)),
417                ..default()
418            },
419            Transform::from_translation(position.extend(990.0)),
420            Particle::explosion_spark(),
421            ParticleBehavior::explosion(),
422            Velocity(velocity),
423        ));
424    }
425}
426
427// =============================================================================
428// AUDIO SYSTEM - Generic Audio Management
429// =============================================================================
430
431#[derive(Resource, Default)]
432pub struct AudioSettings {
433    pub master_volume: f32,
434    pub sfx_volume: f32,
435    pub music_volume: f32,
436}
437
438#[derive(Component)]
439pub struct AudioEffect {
440    pub effect_type: AudioEffectType,
441    pub duration: Option<f32>,
442}
443
444#[derive(Clone)]
445pub enum AudioEffectType {
446    BackgroundMusic,
447    Looping(String),
448    OneShot(String),
449}
450
451#[derive(Resource, Default)]
452pub struct AudioState {
453    pub background_music_entity: Option<Entity>,
454    pub is_muted: bool,
455}
456
457pub fn play_sound_effect(
458    commands: &mut Commands,
459    asset_server: &AssetServer,
460    audio_settings: &AudioSettings,
461    audio_path: &str,
462    volume_multiplier: f32,
463    audio_state: &AudioState,
464) {
465    if audio_state.is_muted {
466        return;
467    }
468
469    commands.spawn((
470        AudioPlayer::<AudioSource>(asset_server.load(audio_path.to_string())),
471        PlaybackSettings {
472            mode: PlaybackMode::Despawn,
473            volume: Volume::Linear(
474                volume_multiplier * audio_settings.sfx_volume * audio_settings.master_volume,
475            ),
476            ..default()
477        },
478        AudioEffect {
479            effect_type: AudioEffectType::OneShot(audio_path.to_string()),
480            duration: Some(1.0),
481        },
482    ));
483}
484
485pub fn audio_controls(
486    keyboard_input: Res<ButtonInput<KeyCode>>,
487    mut audio_settings: ResMut<AudioSettings>,
488    mut audio_state: ResMut<AudioState>,
489) {
490    if keyboard_input.just_pressed(KeyCode::KeyM) {
491        audio_state.is_muted = !audio_state.is_muted;
492    }
493
494    if keyboard_input.just_pressed(KeyCode::Equal) {
495        audio_settings.master_volume = (audio_settings.master_volume + 0.1).clamp(0.0, 1.0);
496    }
497
498    if keyboard_input.just_pressed(KeyCode::Minus) {
499        audio_settings.master_volume = (audio_settings.master_volume - 0.1).clamp(0.0, 1.0);
500    }
501}
502
503// =============================================================================
504// PARALLAX BACKGROUND SYSTEM
505// =============================================================================
506
507#[derive(Component)]
508pub struct ParallaxLayer {
509    pub speed_multiplier: f32,
510    pub layer_depth: f32,
511    pub wrap_distance: f32,
512    pub original_position: Vec2,
513}
514
515#[derive(Resource, Default)]
516pub struct CameraMovement {
517    pub last_position: Vec2,
518    pub current_velocity: Vec2,
519}
520
521pub fn update_camera_movement(
522    mut camera_movement: ResMut<CameraMovement>,
523    camera_query: Query<&Transform, With<Camera>>,
524    time: Res<Time>,
525) {
526    if let Ok(camera_transform) = camera_query.single() {
527        let current_pos = camera_transform.translation.truncate();
528        camera_movement.current_velocity =
529            (current_pos - camera_movement.last_position) / time.delta_secs();
530        camera_movement.last_position = current_pos;
531    }
532}
533
534pub fn update_parallax_layers(
535    mut parallax_query: Query<(&mut Transform, &mut ParallaxLayer)>,
536    camera_movement: Res<CameraMovement>,
537    time: Res<Time>,
538) {
539    let camera_delta = camera_movement.current_velocity * time.delta_secs();
540
541    for (mut transform, layer) in parallax_query.iter_mut() {
542        let parallax_movement = -camera_delta * layer.speed_multiplier;
543        transform.translation += parallax_movement.extend(0.0);
544
545        let camera_pos = camera_movement.last_position;
546        let layer_pos = transform.translation.truncate();
547        let distance = (layer_pos - camera_pos).length();
548
549        if distance > layer.wrap_distance {
550            let direction_to_camera = (camera_pos - layer_pos).normalize();
551            transform.translation = (camera_pos - direction_to_camera * layer.wrap_distance * 0.8)
552                .extend(layer.layer_depth);
553        }
554    }
555}
556
557// =============================================================================
558// INPUT UTILITIES
559// =============================================================================
560
561#[derive(Resource, Default)]
562pub struct MouseWorldPos(pub Vec2);
563
564pub fn update_mouse_world_position(
565    mut mouse_world_pos: ResMut<MouseWorldPos>,
566    q_window: Query<&Window, With<PrimaryWindow>>,
567    q_camera: Query<(&Camera, &GlobalTransform)>,
568) {
569    let Ok(window) = q_window.single() else {
570        return;
571    };
572
573    for (camera, camera_transform) in q_camera.iter() {
574        if let Some(cursor_pos) = window.cursor_position() {
575            if let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_pos) {
576                mouse_world_pos.0 = ray.origin.truncate();
577                break;
578            }
579        }
580    }
581}
582
583pub fn toggle_fullscreen(
584    keyboard_input: Res<ButtonInput<KeyCode>>,
585    mut windows: Query<&mut Window, With<PrimaryWindow>>,
586) {
587    if keyboard_input.just_pressed(KeyCode::F11) {
588        if let Ok(mut window) = windows.single_mut() {
589            window.mode = match window.mode {
590                WindowMode::Windowed => WindowMode::BorderlessFullscreen(MonitorSelection::Current),
591                _ => WindowMode::Windowed,
592            };
593        }
594    }
595}
596
597// =============================================================================
598// PHYSICS HELPERS
599// =============================================================================
600
601pub fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res<Time>) {
602    for (mut transform, velocity) in query.iter_mut() {
603        transform.translation += velocity.0.extend(0.0) * time.delta_secs();
604    }
605}
606
607pub fn cleanup_distant_entities<T: Component>(
608    mut commands: Commands,
609    entity_query: Query<(Entity, &Transform), With<T>>,
610    camera_query: Query<&Transform, With<Camera>>,
611    max_distance: f32,
612) {
613    if let Ok(camera_transform) = camera_query.single() {
614        let camera_pos = camera_transform.translation.truncate();
615
616        for (entity, transform) in entity_query.iter() {
617            let entity_pos = transform.translation.truncate();
618            if (entity_pos - camera_pos).length() > max_distance {
619                commands.entity(entity).despawn();
620            }
621        }
622    }
623}
624
625// =============================================================================
626// PERFORMANCE MONITORING
627// =============================================================================
628
629pub fn monitor_fps(diagnostics: Res<DiagnosticsStore>, keyboard_input: Res<ButtonInput<KeyCode>>) {
630    if keyboard_input.just_pressed(KeyCode::F12) {
631        if let Some(fps_diagnostic) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
632            if let Some(fps_smoothed) = fps_diagnostic.smoothed() {
633                println!("FPS: {:.2}", fps_smoothed);
634            }
635        }
636    }
637}
638
639// =============================================================================
640// CAMERA SETUP HELPER
641// =============================================================================
642
643pub fn setup_cinematic_camera(mut commands: Commands) {
644    let camera_position = Vec3::new(0.0, 0.0, 1000.0);
645
646    commands.spawn((
647        Camera2d::default(),
648        Transform::from_translation(camera_position),
649        Camera::default(),
650        Hdr,
651        Bloom {
652            intensity: 0.3,
653            low_frequency_boost: 0.7,
654            low_frequency_boost_curvature: 0.95,
655            high_pass_frequency: 1.0,
656            prefilter: bevy::post_process::bloom::BloomPrefilter {
657                threshold: 0.8,
658                threshold_softness: 0.5,
659            },
660            composite_mode: BloomCompositeMode::Additive,
661            ..Default::default()
662        },
663        Tonemapping::TonyMcMapface,
664        Fxaa::default(),
665        DebandDither::Enabled,
666        CinematicCamera {
667            base_position: camera_position,
668            ..default()
669        },
670    ));
671}
672
673// =============================================================================
674// PLUGIN - Bundle everything together
675// =============================================================================
676
677pub struct GameDevToolsPlugin;
678
679impl Plugin for GameDevToolsPlugin {
680    fn build(&self, app: &mut App) {
681        app.insert_resource(GameDevConfig::default())
682            .insert_resource(ScreenShake::default())
683            .insert_resource(AudioSettings {
684                master_volume: 0.7,
685                sfx_volume: 0.8,
686                music_volume: 0.6,
687            })
688            .insert_resource(AudioState::default())
689            .insert_resource(MouseWorldPos::default())
690            .insert_resource(CameraMovement::default())
691            .add_systems(
692                Update,
693                (
694                    unified_camera_shake_system,
695                    update_particles,
696                    apply_velocity,
697                    update_camera_movement,
698                    update_parallax_layers,
699                    update_mouse_world_position,
700                    toggle_fullscreen,
701                    audio_controls,
702                    monitor_fps,
703                ),
704            );
705    }
706}