1#![allow(dead_code, clippy::missing_const_for_fn, unused_imports)]
2
3use 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#[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#[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#[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#[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#[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#[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#[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
597pub 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
625pub 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
639pub 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
673pub 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}