Загрузка данных


use std::collections::{HashMap, HashSet};

use bevy::{asset::RenderAssetUsages, prelude::*};
use bevy_rapier3d::prelude::{Collider, KinematicCharacterController, RigidBody};

use crate::core::{
    app_state::AppState,
    systems::camera::{
        controller::{CameraFixed, CameraMode, CameraRig},
        focus::CameraFocusable,
    },
    utils::attachment::Attached,
};

#[derive(Resource, Default)]
pub struct World {
    chunks: HashMap<IVec2, Entity>,
    ground_material: Option<Handle<StandardMaterial>>,
}

const CHUNK_SIZE: i32 = 16;
const TILE_GAP: f32 = 0.025;

#[derive(Component)]
pub(crate) struct TerrainChunk;

// =================
pub struct WorldTaskManager;

/*
    1. Происходи инициализация загрузки мира
    1.1 Поиск файлов мира и их загрузка
    1.2 Прогрузка начальной области (территории, объектов и сущностей)
    1.3 Прочая прогрузка (как игрок и т.д.)
    2. Выгрузка мира
    2.1 Сохранение мира в файл путём парсинга (сериализации данных в тот или иной вид)
*/

// TESTING
#[derive(Component)]
#[require(Name = auto_name())]
pub struct Player;

fn auto_name() -> Name {
    Name::new("Player")
}

// /TESTING

impl WorldTaskManager {
    /// Функция для загрузки данных мира
    pub(crate) fn setup(
        mut world: ResMut<World>,
        mut _next_state: ResMut<NextState<AppState>>,
        mut commands: Commands,
        mut meshes: ResMut<Assets<Mesh>>,
        mut materials: ResMut<Assets<StandardMaterial>>,
        mut fix: ResMut<CameraFixed>,
        mut camera_rig: ResMut<CameraRig>,
        settings: Res<crate::core::utils::settings::Settings>,
        _asset_server: Res<AssetServer>,
    ) {
        world.ground_material = Some(materials.add(StandardMaterial {
            base_color: Color::WHITE,
            perceptual_roughness: 1.0,
            ..default()
        }));

        let player = commands
            .spawn((
                Mesh3d(meshes.add(Capsule3d::new(0.3, 0.8))),
                MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
                Transform::from_xyz(0.0, 0.7, 0.0),
                RigidBody::KinematicPositionBased,
                Collider::capsule_y(0.4, 0.3),
                KinematicCharacterController {
                    up: Vec3::Y,
                    ..default()
                },
                Player,
                CameraFocusable,
            ))
            .id();

        fix.set_entity_to_fixed(&player);
        camera_rig.mode = CameraMode::Fixed;
        // camera_rig.view_direction = Vec3::Y;
        camera_rig.zoom = 12.0;

        commands.spawn((
            Name::new("Light"),
            PointLight {
                radius: 64.,
                ..default()
            },
            Transform::from_xyz(3.0, 8.0, 5.0),
            Attached::with_offset(player, Transform::from_xyz(0.0, 0.0, -2.0)),
        ));

        Self::update_chunks_inner(
            &mut commands,
            &mut world,
            &mut meshes,
            IVec2::ZERO,
            settings.rendering_distance.max(0),
        );
    }

    /// Синхронизирует область загруженных чанков с позицией игрока.
    pub(crate) fn update_chunks(
        mut commands: Commands,
        mut world: ResMut<World>,
        mut meshes: ResMut<Assets<Mesh>>,
        settings: Res<crate::core::utils::settings::Settings>,
        player: Query<&Transform, With<Player>>,
    ) {
        let Ok(player) = player.single() else {
            return;
        };
        let tile_position = player.translation.xz().floor().as_ivec2();
        let center = tile_position.div_euclid(IVec2::splat(CHUNK_SIZE));
        let radius = settings.rendering_distance.max(0);

        Self::update_chunks_inner(&mut commands, &mut world, &mut meshes, center, radius);
    }

    /// Создаёт недостающие чанки и удаляет вышедшие за радиус загрузки.
    fn update_chunks_inner(
        commands: &mut Commands,
        world: &mut World,
        meshes: &mut Assets<Mesh>,
        center: IVec2,
        radius: i32,
    ) {
        let desired: HashSet<_> = (-radius..=radius)
            .flat_map(|x| (-radius..=radius).map(move |z| center + IVec2::new(x, z)))
            .collect();

        for position in world.chunks.keys().copied().collect::<Vec<_>>() {
            if !desired.contains(&position) {
                if let Some(entity) = world.chunks.remove(&position) {
                    commands.entity(entity).despawn();
                }
            }
        }

        let Some(material) = world.ground_material.clone() else {
            return;
        };
        for position in desired {
            if world.chunks.contains_key(&position) {
                continue;
            }

            let mesh = meshes.add(Self::build_chunk_mesh(position));
            let entity = commands
                .spawn((
                    Name::new(format!("Terrain [{}, {}]", position.x, position.y)),
                    TerrainChunk,
                    Mesh3d(mesh),
                    MeshMaterial3d(material.clone()),
                ))
                .id();
            commands.entity(entity).with_children(|parent| {
                parent.spawn((
                    Name::new("Terrain Collider"),
                    RigidBody::Fixed,
                    Collider::cuboid(8.0, 0.1, 8.0),
                    Transform::from_xyz(
                        (position.x * CHUNK_SIZE) as f32 + 8.0,
                        -0.1,
                        (position.y * CHUNK_SIZE) as f32 + 8.0,
                    ),
                ));
            });
            world.chunks.insert(position, entity);
        }
    }

    pub(crate) fn teardown(
        mut commands: Commands,
        entities: Query<Entity, Or<(With<Player>, With<TerrainChunk>, With<Attached>)>>,
        mut world: ResMut<World>,
    ) {
        for entity in &entities {
            commands.entity(entity).despawn();
        }
        *world = World::default();
    }

    /// Собирает все клетки чанка в один mesh, не создавая сущность на каждую клетку.
    fn build_chunk_mesh(chunk: IVec2) -> Mesh {
        let mut positions = Vec::with_capacity((CHUNK_SIZE * CHUNK_SIZE * 4) as usize);
        let mut normals = Vec::with_capacity(positions.capacity());
        let mut colors = Vec::with_capacity(positions.capacity());
        let mut indices = Vec::with_capacity((CHUNK_SIZE * CHUNK_SIZE * 6) as usize);

        for z in 0..CHUNK_SIZE {
            for x in 0..CHUNK_SIZE {
                let base = positions.len() as u32;
                let x0 = (chunk.x * CHUNK_SIZE + x) as f32 + TILE_GAP;
                let z0 = (chunk.y * CHUNK_SIZE + z) as f32 + TILE_GAP;
                let x1 = x0 + 1.0 - TILE_GAP * 2.0;
                let z1 = z0 + 1.0 - TILE_GAP * 2.0;
                positions.extend_from_slice(&[
                    [x0, 0.0, z0],
                    [x1, 0.0, z0],
                    [x1, 0.0, z1],
                    [x0, 0.0, z1],
                ]);
                normals.extend_from_slice(&[[0.0, 1.0, 0.0]; 4]);

                let shade = if (x + z + chunk.x + chunk.y).rem_euclid(2) == 0 {
                    0.30
                } else {
                    0.36
                };
                colors.extend_from_slice(&[[shade, 0.40, 0.27, 1.0]; 4]);
                indices.extend_from_slice(&[base, base + 2, base + 1, base, base + 3, base + 2]);
            }
        }

        Mesh::new(
            bevy::mesh::PrimitiveTopology::TriangleList,
            RenderAssetUsages::default(),
        )
        .with_inserted_indices(bevy::mesh::Indices::U32(indices))
        .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
        .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
        .with_inserted_attribute(Mesh::ATTRIBUTE_COLOR, colors)
    }
}