merosity 0.1.0

(wip) competitive stacker game
// merosity, online stacker game
//
// Copyright (c) 2023 rini
// SPDX-License-Identifier: Apache-2.0

use std::sync::Arc;
use std::time::Duration;

use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, ShaderRef};
use bevy::sprite::{Material2d, Material2dPlugin, MaterialMesh2dBundle};

use crate::engine::{Engine, Game, Turn};
use crate::GameState;

#[derive(Default, Resource)]
struct Lobby {
    engine: Arc<Engine>,
    games: Vec<Entity>,
}

#[derive(Component)]
struct Controlled;

#[derive(Component, Clone)]
struct Grid {
    shape: (usize, usize),
    tiles: Vec2,
    center: Vec2,
}

impl Grid {
    fn new(shape: (usize, usize)) -> Self {
        let (width, height) = shape;
        let tiles = Vec2::new(width as f32, height as f32 / 2.);
        let center = tiles / 2. - 0.5;

        Grid {
            shape,
            tiles,
            center,
        }
    }
}

#[derive(Component)]
struct Cell {
    x: usize,
    y: usize,
}

#[derive(Component)]
struct Part {
    i: usize,
}

#[derive(Component)]
struct Ghost;

#[derive(Debug, Clone, Asset, TypePath, AsBindGroup)]
struct BoardMaterial {
    #[uniform(0)]
    color: Color,
    #[uniform(1)]
    tiles: Vec4,
}

impl BoardMaterial {
    fn new(color: Color, tiles: Vec2) -> Self {
        Self {
            color,
            // padding necessary for webgl2
            tiles: (tiles, Vec2::ZERO).into(),
        }
    }
}

impl Material2d for BoardMaterial {
    fn fragment_shader() -> ShaderRef {
        "shaders/board.wgsl".into()
    }
}

fn setup(
    engine: Res<Engine>,
    mut cmd: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut boards: ResMut<Assets<BoardMaterial>>,
    ass: Res<AssetServer>,
    mut events: EventWriter<GameEvent>,
) {
    let grid = Grid::new(engine.shape);

    cmd.insert_resource(GameAssets {
        mino_sprite: ass.load("mino.png"),
        board_mesh: meshes.add(Mesh::from(shape::Quad::new(grid.tiles * 60.))),
        grid_material: boards.add(BoardMaterial::new(Color::rgb(0.2, 0.2, 0.2), grid.tiles)),
        outline_material: boards.add(BoardMaterial::new(
            Color::rgb(0.8, 0.8, 0.8),
            Vec2::splat(1.),
        )),
    });

    cmd.init_resource::<Lobby>();

    events.send(GameEvent::Create {
        controlled: true,
        grid: grid.clone(),
    });

    events.send(GameEvent::Create {
        controlled: false,
        grid,
    });
}

#[derive(Resource)]
struct GameAssets {
    mino_sprite: Handle<Image>,
    board_mesh: Handle<Mesh>,
    grid_material: Handle<BoardMaterial>,
    outline_material: Handle<BoardMaterial>,
}

#[derive(Event)]
enum GameEvent {
    Create { controlled: bool, grid: Grid },
    Remove { entity: Entity },
}

fn manage_games(
    mut cmd: Commands,
    assets: Res<GameAssets>,
    mut lobby: ResMut<Lobby>,
    mut events: EventReader<GameEvent>,
) {
    for event in events.read() {
        match event {
            GameEvent::Create { controlled, grid } => {
                let (width, height) = grid.shape;
                let center = grid.center;

                let sprite = SpriteBundle {
                    sprite: Sprite {
                        color: Color::rgba(0., 0., 0., 0.),
                        ..Default::default()
                    },
                    texture: assets.mino_sprite.clone(),
                    ..Default::default()
                };

                let game = Game::new(lobby.engine.clone());
                let mut parent = cmd.spawn((game, grid.clone(), SpatialBundle::default()));

                lobby.games.push(parent.id());

                if *controlled {
                    parent.insert(Controlled);
                }

                // todo: oh dear god refactor this
                parent.with_children(|cmd| {
                    for x in 0..width {
                        for y in 0..height {
                            let mut sprite = sprite.clone();

                            sprite.transform.translation =
                                ((Vec2::new(x as f32, y as f32) - center) * 60.).extend(0.);

                            cmd.spawn((sprite, Cell { x, y }));
                        }
                    }

                    for i in 0..4 {
                        cmd.spawn((sprite.clone(), Part { i }));
                        cmd.spawn((sprite.clone(), Part { i }, Ghost));
                    }

                    cmd.spawn(MaterialMesh2dBundle {
                        mesh: assets.board_mesh.clone().into(),
                        material: assets.grid_material.clone(),
                        ..Default::default()
                    });

                    cmd.spawn(MaterialMesh2dBundle {
                        mesh: assets.board_mesh.clone().into(),
                        material: assets.outline_material.clone(),
                        ..Default::default()
                    });
                });
            }
            GameEvent::Remove { entity } => {
                cmd.entity(*entity).despawn_recursive();
                lobby.games.retain(|e| e != entity);
            }
        }
    }
}

fn reorder_games(
    lobby: Res<Lobby>,
    camera: Query<(&Camera, &GlobalTransform)>,
    mut games: Query<&mut Transform, With<Game>>,
) {
    let (cam, trans) = camera.single();
    let width = cam.ndc_to_world(trans, Vec3::X).unwrap().x * 2.;

    for (i, game) in lobby.games.iter().enumerate() {
        if let Ok(mut transform) = games.get_mut(*game) {
            transform.translation = match lobby.games.len() {
                1 => Vec3::ZERO,
                c => Vec3::X * space_evenly(c, width, 660., i),
            };
        }
    }
}

fn space_evenly(count: usize, width: f32, item_width: f32, i: usize) -> f32 {
    let c = count as f32;
    (width - item_width * c) / (c + 1.) * (i as f32 + 1.) + item_width * (i as f32 + 0.5)
        - width / 2.
}

fn update_tiles(
    games: Query<(&Game, &Children)>,
    mut cells: Query<(&Cell, &mut Sprite), Without<Part>>,
    mut parts: Query<(Option<&Ghost>, &mut Sprite), With<Part>>,
) {
    for (game, children) in &games {
        for child in children {
            if let Ok((cell, mut sprite)) = cells.get_mut(*child) {
                sprite.color = Color::from(&game.board[cell.y][cell.x]);
            }

            if let Ok((ghost, mut sprite)) = parts.get_mut(*child) {
                sprite.color = if ghost.is_some() {
                    Color::rgba(0.9, 0.9, 0.9, 0.8)
                } else {
                    Color::from(&game.piece.kind)
                };
            }
        }
    }
}

fn update_pieces(
    games: Query<(&Game, &Grid, &Children)>,
    mut parts: Query<(&Part, &mut Transform), Without<Ghost>>,
    mut ghosts: Query<(&Part, &mut Transform), With<Ghost>>,
) {
    for (game, grid, children) in &games {
        let center = grid.center;
        let ghost = game.ghost();

        for child in children {
            if let Ok((part, mut transform)) = parts.get_mut(*child) {
                let (x, y) = game.piece.parts[part.i];
                transform.translation = ((Vec2::new(x as f32, y as f32) - center) * 60.).extend(0.);
            }

            if let Ok((part, mut transform)) = ghosts.get_mut(*child) {
                let (x, mut y) = game.piece.parts[part.i];
                y -= ghost;
                transform.translation = ((Vec2::new(x as f32, y as f32) - center) * 60.).extend(0.);
            }
        }
    }
}

fn kill_players(games: Query<(Entity, &Game)>, mut events: EventWriter<GameEvent>) {
    for (entity, game) in &games {
        if game.lockout {
            events.send(GameEvent::Remove { entity });
        }
    }
}

struct AutoShift {
    das: Timer,
    arr: Timer,
}

impl Default for AutoShift {
    fn default() -> Self {
        let mut das = Timer::new(Duration::from_millis(150), TimerMode::Once);
        let arr = Timer::new(Duration::from_millis(33), TimerMode::Repeating);
        das.pause();

        Self { das, arr }
    }
}

fn handle_input(
    mut game: Query<&mut Game, With<Controlled>>,
    keys: Res<Input<KeyCode>>,
    time: Res<Time>,
    mut shift: Local<AutoShift>,
) {
    let Ok(mut game) = game.get_single_mut() else {
        return;
    };

    shift.das.tick(time.delta());
    shift.arr.tick(time.delta());

    if keys.just_pressed(KeyCode::A) {
        shift.das.reset();
        shift.das.unpause();
        game.translate(-1, 0);
    }
    if keys.just_pressed(KeyCode::D) {
        shift.das.reset();
        shift.das.unpause();
        game.translate(1, 0);
    }

    if shift.das.just_finished() {
        shift.arr.reset();
    }

    if shift.das.just_finished() || shift.das.finished() && shift.arr.just_finished() {
        if keys.pressed(KeyCode::A) {
            game.translate(-1, 0);
        }
        if keys.pressed(KeyCode::D) {
            game.translate(1, 0);
        }
    }

    if keys.just_pressed(KeyCode::Space) {
        game.hard_drop();
    }
    if keys.just_pressed(KeyCode::S) {
        game.translate(0, -1);
    }
    if keys.just_pressed(KeyCode::J) {
        game.rotate(Turn::Ccw);
    }
    if keys.just_pressed(KeyCode::K) {
        game.rotate(Turn::Cw);
    }
}

fn very_smart_bot(mut games: Query<&mut Game, Without<Controlled>>) {
    for mut game in &mut games {
        if fastrand::u8(0..120) == 1 {
            game.hard_drop();
        }
    }
}

pub struct GameScene;

impl Plugin for GameScene {
    fn build(&self, app: &mut App) {
        app.add_plugins(Material2dPlugin::<BoardMaterial>::default())
            .add_event::<GameEvent>()
            .add_systems(OnEnter(GameState::Gaming), setup)
            .add_systems(
                Update,
                (
                    manage_games,
                    reorder_games,
                    update_tiles,
                    update_pieces,
                    handle_input,
                    very_smart_bot,
                    kill_players,
                )
                    .run_if(in_state(GameState::Gaming)),
            );
    }
}