1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
use bevy::{
    app::AppExit,
    prelude::{Text as BevyText, *},
    time::Time,
    utils::HashMap,
    window::{close_on_esc, PrimaryWindow, WindowPlugin},
};
use bevy_prototype_lyon::prelude::*;
use std::{
    ops::{Deref, DerefMut},
    path::PathBuf,
    time::Duration,
};

use crate::{
    audio::AudioManager,
    mouse::{CursorMoved, MouseButtonInput, MouseMotion, MousePlugin, MouseWheel},
    prelude::{
        AudioManagerPlugin, CollisionEvent, KeyboardInput, KeyboardPlugin, KeyboardState,
        MouseState, PhysicsPlugin,
    },
    sprite::Sprite,
    text::Text,
};

// Public re-export
pub use bevy::window::{Cursor, Window, WindowMode, WindowResolution};

/// Engine is the primary way that you will interact with Rusty Engine. Each frame this struct
/// is provided to the "logic" functions (or closures) that you provided to [`Game::add_logic`]. The
/// fields in this struct are divided into two groups:
///
/// 1. `SYNCED` fields.
///
/// These fields are marked with `SYNCED`. These fields are shared between you and the engine. Each
/// frame Rusty Engine will populate these fields, then provide them to the user's game logic
/// function, and then examine any changes the user made and sync those changes back to the engine.
///
/// 2. `INFO` fields
///
/// INFO fields are provided as fresh, readable information to you each frame. Since information in
/// these fields are overwritten every frame, any changes to them are ignored. Thus, you can feel
/// free to, e.g. consume all the events out of the `collision_events` vector.
#[derive(Default, Debug, Resource)]
pub struct Engine {
    /// SYNCED - The state of all sprites this frame. To add a sprite, use the
    /// [`add_sprite`](Engine::add_sprite) method. Modify & remove sprites as you like.
    pub sprites: HashMap<String, Sprite>,
    /// SYNCED - The state of all texts this frame. For convenience adding a text, use the
    /// [`add_text`](Engine::add_text) method. Modify & remove text as you like.
    pub texts: HashMap<String, Text>,
    /// SYNCED - If set to `true`, the game exits. Note: the current frame will run to completion first.
    pub should_exit: bool,
    /// SYNCED - If set to `true`, then debug lines are shown depicting sprite colliders
    pub show_colliders: bool,
    // so we can tell if the value changed this frame
    last_show_colliders: bool,
    /// INFO - All the collision events that occurred this frame. For collisions to be generated
    /// between sprites, both sprites must have [`Sprite.collision`] set to `true` and both sprites
    /// must have colliders (use the collider example to create a collider for your own images).
    /// Collision events are generated when two sprites' colliders begin or end overlapping in 2D
    /// space.
    pub collision_events: Vec<CollisionEvent>,
    /// INFO - The current state of mouse location and buttons. Useful for input handling that only
    /// cares about the final state of the mouse each frame, and not the intermediate states.
    pub mouse_state: MouseState,
    /// INFO - All the mouse button events that occurred this frame.
    pub mouse_button_events: Vec<MouseButtonInput>,
    /// INFO - All the mouse location events that occurred this frame. The events are Bevy
    /// [`CursorMoved`] structs, but despite the name they represent the _location_ of the mouse
    /// during this frame.
    pub mouse_location_events: Vec<CursorMoved>,
    /// INFO - All the mouse motion events that occurred this frame. These represent the relative
    /// movements of the mouse, not the location of the mouse.
    pub mouse_motion_events: Vec<MouseMotion>,
    /// INFO - All the mouse wheel events that occurred this frame.
    pub mouse_wheel_events: Vec<MouseWheel>,
    /// INFO - The current state of all the keys on the keyboard. Use this to control movement in
    /// your games!  A [`KeyboardState`] has helper methods you should use to query the state of
    /// specific [`KeyCode`](crate::prelude::KeyCode)s.
    pub keyboard_state: KeyboardState,
    /// INFO - All the keyboard input events. These are text-processor-like events. If you are
    /// looking for keyboard events to control movement in a game character, you should use
    /// [`Engine::keyboard_state`] instead. For example, one pressed event will fire when you
    /// start holding down a key, and then after a short delay additional pressed events will occur
    /// at the same rate that additional letters would show up in a word processor. When the key is
    /// finally released, a single released event is emitted.
    pub keyboard_events: Vec<KeyboardInput>,
    /// INFO - The delta time (time between frames) for the current frame as a [`Duration`], perfect
    /// for use with [`Timer`](crate::prelude::Timer)s
    pub delta: Duration,
    /// INFO - The delta time (time between frames) for the current frame as an [`f32`], perfect for
    /// use in math with other `f32`'s. A cheap and quick way to approximate smooth movement
    /// (velocity, accelleration, etc.) is to multiply it by `delta_f32`.
    pub delta_f32: f32,
    /// INFO - The amount of time the game has been running since startup as a [`Duration`]
    pub time_since_startup: Duration,
    /// INFO - The amount of time the game has been running as an [`f64`]. This needs to be an f64,
    /// since it gets to be large enough that an f32 would lose precision. For best results, do your
    /// math on the `f64` and get it to a smaller value _before_ casting it to an `f32`.
    pub time_since_startup_f64: f64,
    /// A struct with methods to play sound effects and music
    pub audio_manager: AudioManager,
    /// INFO - Window dimensions in logical pixels. On high DPI screens, there will often be four
    /// physical pixels per logical pixel. On low DPI screens, one logical pixel is one physical
    /// pixel.
    pub window_dimensions: Vec2,
}

impl Engine {
    #[must_use]
    /// Create and add a [`Sprite`] to the game. Use the `&mut Sprite` that is returned to adjust
    /// the translation, rotation, etc. Use a *unique* label for each sprite. Attempting to add two
    /// sprites with the same label will cause a crash.
    pub fn add_sprite<T: Into<String>, P: Into<PathBuf>>(
        &mut self,
        label: T,
        file_or_preset: P,
    ) -> &mut Sprite {
        let label = label.into();
        self.sprites
            .insert(label.clone(), Sprite::new(label.clone(), file_or_preset));
        // Unwrap: Can't crash because we just inserted the sprite
        self.sprites.get_mut(&label).unwrap()
    }

    #[must_use]
    /// Create and add a [`Text`] to the game. Use the `&mut Text` that is returned to adjust the
    /// translation, rotation, etc. Use a *unique* label for each text. Attempting to add two texts
    /// with the same label will cause a crash.
    pub fn add_text<T, S>(&mut self, label: T, text: S) -> &mut Text
    where
        T: Into<String>,
        S: Into<String>,
    {
        let label = label.into();
        let text = text.into();
        let curr_text = Text {
            label: label.clone(),
            value: text,
            ..Default::default()
        };
        self.texts.insert(label.clone(), curr_text);
        // Unwrap: Can't crash because we just inserted the text
        self.texts.get_mut(&label).unwrap()
    }
}

/// startup system - grab window settings, initialize all the starting sprites
#[doc(hidden)]
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut engine: ResMut<Engine>) {
    add_sprites(&mut commands, &asset_server, &mut engine);
    add_texts(&mut commands, &asset_server, &mut engine);
}

/// Add visible lines representing a collider
fn add_collider_lines(commands: &mut Commands, sprite: &mut Sprite) {
    // Add the collider lines, a visual representation of the sprite's collider
    let points = sprite.collider.points(); // will be empty vector if NoCollider
    if points.len() >= 2 {
        let mut path_builder = PathBuilder::new();
        path_builder.move_to(points[0]);
        for point in &points[1..] {
            path_builder.line_to(*point);
        }
        path_builder.close(); // draws the line from the last point to the first point
        let line = path_builder.build();
        let transform = sprite.bevy_transform();
        commands
            .spawn((
                ShapeBundle {
                    path: GeometryBuilder::new().add(&line).build(),
                    spatial: SpatialBundle::from_transform(transform),
                    ..Default::default()
                },
                Stroke::new(Color::WHITE, 1.0 / transform.scale.x),
            ))
            // .spawn(GeometryBuilder::build_as(
            //     &line,
            //     DrawMode::Stroke(StrokeMode::new(Color::WHITE, 1.0 / transform.scale.x)),
            //     transform,
            // ))
            .insert(ColliderLines {
                sprite_label: sprite.label.clone(),
            });
    }
    sprite.collider_dirty = false;
}

/// helper function: Add Bevy components for all the sprites in engine.sprites
#[doc(hidden)]
pub fn add_sprites(commands: &mut Commands, asset_server: &Res<AssetServer>, engine: &mut Engine) {
    for (_, sprite) in engine.sprites.drain() {
        // Create the sprite
        let transform = sprite.bevy_transform();
        let texture_path = sprite.filepath.clone();
        commands.spawn((
            sprite,
            SpriteBundle {
                texture: asset_server.load(texture_path),
                transform,
                ..Default::default()
            },
        ));
    }
}

/// Bevy system which adds any needed Bevy components to correspond to the texts in
/// `engine.texts`
#[doc(hidden)]
pub fn add_texts(commands: &mut Commands, asset_server: &Res<AssetServer>, engine: &mut Engine) {
    for (_, text) in engine.texts.drain() {
        let transform = text.bevy_transform();
        let font_size = text.font_size;
        let text_string = text.value.clone();
        let font_path = text.font.clone();
        commands.spawn((
            text,
            Text2dBundle {
                text: BevyText::from_section(
                    text_string,
                    TextStyle {
                        font: asset_server.load(font_path),
                        font_size,
                        color: Color::WHITE,
                    },
                )
                .with_alignment(TextAlignment::Center),
                transform,
                ..Default::default()
            },
        ));
    }
}

/// system - update current window dimensions in the engine, because people resize windows
#[doc(hidden)]
pub fn update_window_dimensions(
    window_query: Query<&Window, With<PrimaryWindow>>,
    mut engine: ResMut<Engine>,
) {
    // It's possible to not have a window for the first frame or two
    let Ok(window) = window_query.get_single() else {
        return;
    };
    let screen_dimensions = Vec2::new(window.width(), window.height());
    if screen_dimensions != engine.window_dimensions {
        engine.window_dimensions = screen_dimensions;
        debug!("Set window dimensions: {}", engine.window_dimensions);
    }
}

/// Component to add to the collider lines visualizations to link them to the sprite they represent
#[derive(Component)]
#[doc(hidden)]
pub struct ColliderLines {
    sprite_label: String,
}

/// A [`Game`] represents the entire game and its data.
/// By default the game will spawn an empty window, and exit upon Esc or closing of the window.
/// Under the hood, Rusty Engine syncs the game data to [Bevy](https://bevyengine.org/) to power
/// most of the underlying functionality.
///
/// [`Game`] forwards method calls to [`Engine`] when it can, so you should be able to use all
/// of the methods from [`Engine`] on [`Game`] during your game setup in your `main()` function.
pub struct Game<S: Resource + Send + Sync + 'static> {
    app: App,
    engine: Engine,
    logic_functions: LogicFuncVec<S>,
    window: Window,
}

impl<S: Resource + Send + Sync + 'static> Default for Game<S> {
    fn default() -> Self {
        Self {
            app: App::new(),
            engine: Engine::default(),
            logic_functions: LogicFuncVec(vec![]),
            window: Window {
                title: "Rusty Engine".into(),
                ..Default::default()
            },
        }
    }
}

impl<S: Resource + Send + Sync + 'static> Game<S> {
    /// Create an new, empty [`Game`] with an empty [`Engine`]
    pub fn new() -> Self {
        if std::fs::read_dir("assets").is_err() {
            println!("FATAL: Could not find assets directory. Have you downloaded the assets?\nhttps://github.com/CleanCut/rusty_engine#you-must-download-the-assets-separately");
            std::process::exit(1);
        }
        Default::default()
    }

    /// Use this to set properties of the native OS window before running the game. See the
    /// [window](https://github.com/CleanCut/rusty_engine/blob/main/examples/window.rs) example for
    /// more information.
    pub fn window_settings(&mut self, window: Window) -> &mut Self {
        self.window = window;
        self
    }

    /// Start the game.
    pub fn run(&mut self, initial_game_state: S) {
        self.app.insert_resource::<S>(initial_game_state);
        self.app
            // TODO: Remove this to use the new, darker default color once the videos have been remastered
            .insert_resource(ClearColor(Color::rgb(0.4, 0.4, 0.4)))
            // Built-ins
            .add_plugins(
                DefaultPlugins
                    .set(WindowPlugin {
                        primary_window: Some(self.window.clone()),
                        ..Default::default()
                    })
                    .set(ImagePlugin::default_nearest()),
            )
            .add_systems(
                Update,
                (
                    (close_on_esc),
                    (update_window_dimensions, game_logic_sync::<S>),
                ),
            )
            // External Plugins
            .add_plugins(ShapePlugin) // bevy_prototype_lyon, for displaying sprite colliders
            // Rusty Engine Plugins
            .add_plugins((
                AudioManagerPlugin,
                KeyboardPlugin,
                MousePlugin,
                PhysicsPlugin,
            ))
            //.insert_resource(ReportExecutionOrderAmbiguities) // for debugging
            .add_systems(Startup, setup);
        self.app.world.spawn(Camera2dBundle::default());
        let engine = std::mem::take(&mut self.engine);
        self.app.insert_resource(engine);
        let mut logic_functions = LogicFuncVec(vec![]);
        std::mem::swap(&mut self.logic_functions, &mut logic_functions);
        self.app.insert_resource(logic_functions);
        self.app.run();
    }

    /// `logic_function` is a function or closure that takes two parameters and returns nothing:
    ///
    /// - `engine: &mut Engine`
    /// - `game_state`, which is a mutable reference (`&mut`) to the game state struct you defined,
    ///    or `&mut ()` if you didn't define one.
    pub fn add_logic(&mut self, logic_function: fn(&mut Engine, &mut S)) {
        self.logic_functions.0.push(logic_function);
    }
}

/// system - the magic that connects Rusty Engine to Bevy, frame by frame
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
fn game_logic_sync<S: Resource + Send + Sync + 'static>(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut engine: ResMut<Engine>,
    mut game_state: ResMut<S>,
    logic_functions: Res<LogicFuncVec<S>>,
    keyboard_state: Res<KeyboardState>,
    mouse_state: Res<MouseState>,
    time: Res<Time>,
    mut app_exit_events: EventWriter<AppExit>,
    mut collision_events: EventReader<CollisionEvent>,
    mut query_set: ParamSet<(
        Query<(Entity, &mut Sprite, &mut Transform)>,
        Query<(Entity, &mut Text, &mut Transform, &mut BevyText)>,
        Query<(Entity, &mut Stroke, &mut Transform, &ColliderLines)>,
    )>,
) {
    // Update this frame's timing info
    engine.delta = time.delta();
    engine.delta_f32 = time.delta_seconds();
    engine.time_since_startup = time.elapsed();
    engine.time_since_startup_f64 = time.elapsed_seconds_f64();

    // Copy keyboard state over to engine to give to users
    engine.keyboard_state = keyboard_state.clone();

    // Copy mouse state over to engine to give to users
    engine.mouse_state = mouse_state.clone();

    // Copy all collision events over to the engine to give to users
    engine.collision_events.clear();
    for collision_event in collision_events.read() {
        engine.collision_events.push(collision_event.clone());
    }

    // Copy all sprites over to the engine to give to users
    engine.sprites.clear();
    for (_, sprite, _) in query_set.p0().iter() {
        let _ = engine
            .sprites
            .insert(sprite.label.clone(), (*sprite).clone());
    }

    // Copy all texts over to the engine to give to users
    engine.texts.clear();
    for (_, text, _, _) in query_set.p1().iter() {
        let _ = engine.texts.insert(text.label.clone(), (*text).clone());
    }

    // Perform all the user's game logic for this frame
    for func in logic_functions.0.iter() {
        func(&mut engine, &mut game_state);
    }

    if !engine.last_show_colliders && engine.show_colliders {
        // Just turned on show_colliders -- create collider lines for all sprites
        for sprite in engine.sprites.values_mut() {
            add_collider_lines(&mut commands, sprite);
        }
    } else if engine.last_show_colliders && !engine.show_colliders {
        // Just turned off show_colliders -- delete collider lines for all sprites
        for (entity, _, _, _) in query_set.p2().iter_mut() {
            commands.entity(entity).despawn();
        }
    }
    // Update transform & line width of all collider lines
    if engine.show_colliders {
        // Delete collider lines for sprites which are missing, or whose colliders are dirty
        for (entity, _, _, collider_lines) in query_set.p2().iter_mut() {
            if let Some(sprite) = engine.sprites.get(&collider_lines.sprite_label) {
                if sprite.collider_dirty {
                    commands.entity(entity).despawn();
                }
            } else {
                commands.entity(entity).despawn();
            }
        }
        // Add collider lines for sprites whose colliders are dirty
        for sprite in engine.sprites.values_mut() {
            if sprite.collider_dirty {
                add_collider_lines(&mut commands, sprite);
            }
        }
        // Update transform & line width
        for (_, mut stroke, mut transform, collider_lines) in query_set.p2().iter_mut() {
            if let Some(sprite) = engine.sprites.get(&collider_lines.sprite_label) {
                *transform = sprite.bevy_transform();
                // We want collider lines to appear on top of the sprite they are for, so they need a
                // slightly higher z value. We tell users to only use up to 999.0.
                transform.translation.z = (transform.translation.z + 0.1).clamp(0.0, 999.1);
            }
            // Stroke line width gets scaled with the transform, but we want it to appear to be the same
            // regardless of scale, so we have to counter the scale.
            stroke.options.line_width = 1.0 / transform.scale.x;
        }
    }
    engine.last_show_colliders = engine.show_colliders;

    // Transfer any changes in the user's Sprite copies to the Bevy Sprite and Transform components
    for (entity, mut sprite, mut transform) in query_set.p0().iter_mut() {
        if let Some(sprite_copy) = engine.sprites.remove(&sprite.label) {
            *sprite = sprite_copy;
            *transform = sprite.bevy_transform();
        } else {
            commands.entity(entity).despawn();
        }
    }

    // Add Bevy components for any new sprites remaining in engine.sprites
    add_sprites(&mut commands, &asset_server, &mut engine);

    // Transfer any changes in the user's Texts to the Bevy Text and Transform components
    for (entity, mut text, mut transform, mut bevy_text_component) in query_set.p1().iter_mut() {
        if let Some(text_copy) = engine.texts.remove(&text.label) {
            *text = text_copy;
            *transform = text.bevy_transform();
            if text.value != bevy_text_component.sections[0].value {
                bevy_text_component.sections[0].value = text.value.clone();
            }
            #[allow(clippy::float_cmp)]
            if text.font_size != bevy_text_component.sections[0].style.font_size {
                bevy_text_component.sections[0].style.font_size = text.font_size;
            }
            let font = asset_server.load(text.font.clone());
            if bevy_text_component.sections[0].style.font != font {
                bevy_text_component.sections[0].style.font = font;
            }
        } else {
            commands.entity(entity).despawn();
        }
    }

    // Add Bevy components for any new texts remaining in engine.texts
    add_texts(&mut commands, &asset_server, &mut engine);

    if engine.should_exit {
        app_exit_events.send(AppExit);
    }
}

// The Deref and DerefMut implementations make it so that you can call all the `Engine` methods
// on a `Game`, which is much more straightforward for game setup in `main()`
impl<S: Resource + Send + Sync + 'static> Deref for Game<S> {
    type Target = Engine;

    fn deref(&self) -> &Self::Target {
        &self.engine
    }
}

impl<S: Resource + Send + Sync + 'static> DerefMut for Game<S> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.engine
    }
}

#[derive(Resource)]
struct LogicFuncVec<S: Resource + Send + Sync + 'static>(Vec<fn(&mut Engine, &mut S)>);