numples 1.2.0

Yet another colourful sudoku playing game.
Documentation
use bevy::ecs::error::Result;
use bevy::prelude::*;

use crate::consts::{
    CELL_SIZE,
    MAGICAL_AJUSTMENT_NUMBER,
    TITLE_COLOR,
};
use crate::events::NumplesEvent;
use crate::fonts::{MonospaceFont, RegularFont};
use crate::game::{
    Board,
    BoardCell,
    Colors,
    Cursor,
    ErrorCell,
    Level,
    Shapes,
};
use crate::gameover::GameOverCheck;
use crate::gameplay::MustResetClock;
use crate::states::GameState;

use super::clock::ClockDisplay;
use super::paused::{MustUnpause, Paused};
use super::background::BGFlag;
use super::clock::Clock;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Component)]
pub struct Gameplay;

impl Gameplay {
    pub fn update(
        board: Single<&Board>,
        window: Single<&Window>,
        mut display: Single<&mut Text, With<ClockDisplay>>,
        mut cursor: Single<&mut Transform, With<Cursor>>,
        mut error_cells_query: Query<&mut MeshMaterial2d<ColorMaterial>, With<ErrorCell>>,
        mut event_writer: EventWriter<NumplesEvent>,
        time: Res<Time>,
        colors: Res<Colors>,
        mut clock: ResMut<Clock>,
    ) -> Result<()> {
        if !window.focused {
            event_writer.write(NumplesEvent::PauseGame);
        }
        clock.update(&time);
        display.0 = clock.to_string();
        board.update(&mut cursor)?;
        for mut material in &mut error_cells_query {
            let elapsed = time.elapsed_secs_f64() % 0.125;
            material.0 = colors.get(
                if elapsed < 0.0625 {
                    1
                } else {
                    3
                }
            ).clone_weak();
        }
        Ok(())
    }

    pub fn load_gameplay(
        mut commands: Commands,
        level: Single<&Level>,
        paused: Res<Paused>,
        shapes: Res<Shapes>,
        colors: Res<Colors>,
        regular_font: Res<RegularFont>,
        mut clock: ResMut<Clock>,
        must_reset_clock: Query<Entity, With<MustResetClock>>,
        monospace_font: Res<MonospaceFont>,
        mut event_writer: EventWriter<NumplesEvent>,
        board_query: Query<&Board>,
    ) -> Result<()> {
        if paused.0 {
            // Do NOT reload gameplay when unpausing
            commands.spawn(MustUnpause);
            return Ok(());
        }

        if let Ok(must_reset_clock) = must_reset_clock.single() {
            commands.entity(must_reset_clock).despawn();
            clock.reset();
        }
        commands.spawn((
            Self,
            BGFlag,
            Mesh2d(shapes.full_bg_rect.clone_weak()),
            MeshMaterial2d(colors.black().clone_weak()),
            Transform::from_xyz(0.0, 0.0, -10.0),
        ));

        commands.spawn((
            Self,
            Text::new(level.to_string()),
            TextFont {
                font: regular_font.font().clone_weak(),
                font_size: 32.0,
                ..default()
            },
            TextColor(TITLE_COLOR.clone()),
            TextLayout::new_with_justify(JustifyText::Left),
            Node {
                position_type: PositionType::Absolute,
                align_items: AlignItems::Center,
                left: Val::Px(8.0),
                bottom: Val::Px(8.0),
                ..default()
            },
        ));

        for y in 0..3 {
            for x in 0..3 {
                commands.spawn((
                    Self,
                    BGFlag,
                    Mesh2d(shapes.rect.clone_weak()),
                    MeshMaterial2d(colors.white().clone_weak()),
                    Transform {
                        scale: Vec3 { x: 3.0, y: 3.0, z: 1.0 },
                        translation: Vec3 {
                            x: (x as f32 - 1.0) * CELL_SIZE * 3.0,
                            y: (y as f32 - 1.0) * CELL_SIZE * 3.0 + MAGICAL_AJUSTMENT_NUMBER,
                            z: -6.0,
                        },
                        ..default()
                    },
                ));
            }
        }

        for y in 0..9 {
            for x in 0..9 {
                commands.spawn((
                    Self,
                    BGFlag,
                    Mesh2d(shapes.rect.clone_weak()),
                    MeshMaterial2d(colors.background().clone_weak()),
                    Transform {
                        scale: Vec3 { x: 0.9, y: 0.9, z: 1.0 },
                        translation: Vec3 {
                            x: (x as f32 - 4.0) * CELL_SIZE,
                            y: (y as f32 - 4.0) * CELL_SIZE + MAGICAL_AJUSTMENT_NUMBER,
                            z: -5.0,
                        },
                        ..default()
                    },
                ));
            }
        }

        let board_size = if let Ok(board) = board_query.single() {
            board.size()?
        } else {
          let board: Board = (*level.into_inner()).try_into()?;
          let size = board.size()?;
          commands.spawn((Self, board));
          size
        };

        for y in 0..4 {
            commands.spawn((
                Self,
                BGFlag,
                Mesh2d(shapes.horizontal_line.clone_weak()),
                MeshMaterial2d(colors.get(0).clone_weak()),
                Transform::from_xyz(
                    0.0,
                    CELL_SIZE * 3.0 * y as f32 - board_size.y / 2.0 + MAGICAL_AJUSTMENT_NUMBER,
                    -1.0,
                ),
            ));
        }

        for x in 0..4 {
            commands.spawn((
                Self,
                BGFlag,
                Mesh2d(shapes.vertical_line.clone_weak()),
                MeshMaterial2d(colors.get(0).clone_weak()),
                Transform::from_xyz(
                    CELL_SIZE * 3.0 * x as f32 - board_size.x / 2.0,
                    MAGICAL_AJUSTMENT_NUMBER,
                    -1.0,
                ),
            ));
        }

        commands.spawn((
            Self,
            ClockDisplay,
            Text::new(clock.to_string()),
            TextFont {
                font: monospace_font.font().clone_weak(),
                font_size: 32.0,
                ..default()
            },
            TextColor(TITLE_COLOR.clone()),
            TextLayout::new_with_justify(JustifyText::Center),
            Node {
                position_type: PositionType::Absolute,
                right: Val::Px(8.0),
                bottom: Val::Px(16.0),
                height: Val::Px(32.0),
                ..default()
            },
        ));

        event_writer.write(NumplesEvent::RenderBoard);

        Ok(())
    }

    pub fn must_unpause(
        mut commands: Commands,
        must_pause: Single<Entity, With<MustUnpause>>,
        mut visibilities_query: Query<&mut Visibility, With<Self>>,
        mut paused: ResMut<Paused>,
    ) {
        commands.entity(*must_pause).despawn();
        paused.0 = false;
        for mut visibility in &mut visibilities_query {
            *visibility = Visibility::Visible;
        }
    }

    pub fn unload_or_pause(
        mut commands: Commands,
        entities: Query<Entity, With<Self>>,
        mut visibilities: Query<&mut Visibility, With<Self>>,
        paused: Res<Paused>,
        game_over: Res<GameOverCheck>,
    ) {
        if paused.0 {
            // Pause
            for mut visibility in &mut visibilities {
                *visibility = Visibility::Hidden;
            }
        } else if !game_over.0 {
            // Unload
            for entity in &entities {
                commands.entity(entity).despawn();
            }
        }
    }

    pub fn event_handle(
        mut commands: Commands,
        mut board: Single<&mut Board>,
        cell_query: Query<Entity, With<BoardCell>>,
        cursor_query: Query<Entity, With<Cursor>>,
        mut events: EventReader<NumplesEvent>,
        mut next_state: ResMut<NextState<GameState>>,
        mut game_over: ResMut<GameOverCheck>,
        mut paused: ResMut<Paused>,
        shapes: Res<Shapes>,
        colors: Res<Colors>,
    ) -> Result<()> {
        for event in events.read() {
            match event {
                NumplesEvent::AbortGame => {
                    next_state.set(GameState::Title);
                    return Ok(());
                }

                NumplesEvent::PauseGame => {
                    paused.0 = true;
                    next_state.set(GameState::Paused);
                    return Ok(());
                }

                NumplesEvent::RenderBoard => {
                    board.render(
                        0.0,
                        MAGICAL_AJUSTMENT_NUMBER,
                        &mut commands,
                        &cell_query,
                        &cursor_query,
                        &shapes,
                        &colors,
                    )?;
                    if board.done()? {
                        game_over.0 = true;
                        next_state.set(GameState::GameOver);
                    }
                    return Ok(());
                }

                _ => (),
            }
        }
        Ok(())
    }
}