rustial-renderer-bevy 0.0.1

Bevy Engine renderer for the rustial 2.5D map engine
// ---------------------------------------------------------------------------
//! # Placeholder entity sync system
//!
//! Synchronises the engine's loading-placeholder list to Bevy mesh
//! entities each frame.
//!
//! ## Entity lifecycle
//!
//! Placeholder entities are **ephemeral**: every frame the system
//! despawns all existing placeholder entities and spawns fresh ones
//! from the current [`MapState::loading_placeholders`] list.  This
//! is simpler than a diff-based approach because the placeholder set
//! changes frequently as tiles finish loading.
//!
//! ## Mesh geometry
//!
//! Each placeholder is a flat quad (two triangles) lying at Z = -0.01
//! (slightly below the tile plane) so that loaded tile quads occlude
//! them via Bevy's depth test.  Vertex positions are baked
//! camera-relative at spawn time.
//!
//! ## Material
//!
//! Placeholders use an unlit [`StandardMaterial`] with the background
//! colour from [`PlaceholderStyle`](rustial_engine::PlaceholderStyle)
//! and the shimmer opacity multiplier applied via `base_color.a`.
//!
//! ## Scheduling
//!
//! Registered in [`Update`](bevy::prelude::Update) in the
//! `PainterSet::OpaqueScene` set, alongside tile and vector sync.
// ---------------------------------------------------------------------------

use crate::components::PlaceholderEntity;
use crate::plugin::MapStateResource;
use crate::systems::frame_change_detection::{frame_unchanged, FrameChangeDetection};
use bevy::mesh::{Indices, PrimitiveTopology};
use bevy::prelude::*;
use glam::DVec3;
use rustial_engine::TileId;
use std::collections::HashSet;

/// Synchronise the engine's loading-placeholder tiles to Bevy mesh entities.
///
/// See the [module-level documentation](self) for the full entity
/// lifecycle and mesh geometry.
pub fn sync_placeholders(
    mut commands: Commands,
    state: Res<MapStateResource>,
    mut existing: Query<(
        Entity,
        &PlaceholderEntity,
        &mut Transform,
        &MeshMaterial3d<StandardMaterial>,
    )>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    detection: Res<FrameChangeDetection>,
) {
    // Whole-frame skip: if nothing in the engine changed, placeholder
    // sync has no work to do.
    if frame_unchanged(&detection, &state.0) {
        return;
    }

    let placeholders = state.0.loading_placeholders();
    let style = state.0.placeholder_style();
    let camera_origin: DVec3 = state.0.scene_world_origin();

    // Build the desired tile set for diff comparison.
    let desired_tiles: HashSet<TileId> = placeholders.iter().map(|ph| ph.tile).collect();
    let existing_tiles: HashSet<TileId> = existing.iter().map(|(_, ph, _, _)| ph.tile_id).collect();

    if desired_tiles == existing_tiles {
        // Same tile set -- just update transforms and material colours
        // (shimmer animation) without despawn/respawn churn.
        for (_, ph, mut transform, mat_handle) in existing.iter_mut() {
            if let Some(placeholder) = placeholders.iter().find(|p| p.tile == ph.tile_id) {
                let min = placeholder.bounds.min.position;
                let max = placeholder.bounds.max.position;
                let cx = (min.x + max.x) * 0.5;
                let cy = (min.y + max.y) * 0.5;
                transform.translation.x = (cx - camera_origin.x) as f32;
                transform.translation.y = (cy - camera_origin.y) as f32;

                let opacity = style.shimmer_opacity(placeholder.animation_phase);
                if let Some(material) = materials.get_mut(&mat_handle.0) {
                    material.base_color = Color::linear_rgba(
                        style.background_color[0],
                        style.background_color[1],
                        style.background_color[2],
                        style.background_color[3] * opacity,
                    );
                }
            }
        }
        return;
    }

    // Tile set changed -- despawn stale and spawn new.
    for (entity, ph, _, _) in existing.iter() {
        if !desired_tiles.contains(&ph.tile_id) {
            commands.entity(entity).despawn();
        }
    }

    if placeholders.is_empty() {
        return;
    }

    for ph in placeholders {
        if existing_tiles.contains(&ph.tile) {
            // Already exists; update transform + colour.
            for (_, existing_ph, mut transform, mat_handle) in existing.iter_mut() {
                if existing_ph.tile_id == ph.tile {
                    let min = ph.bounds.min.position;
                    let max = ph.bounds.max.position;
                    let cx = (min.x + max.x) * 0.5;
                    let cy = (min.y + max.y) * 0.5;
                    transform.translation.x = (cx - camera_origin.x) as f32;
                    transform.translation.y = (cy - camera_origin.y) as f32;

                    let opacity = style.shimmer_opacity(ph.animation_phase);
                    if let Some(material) = materials.get_mut(&mat_handle.0) {
                        material.base_color = Color::linear_rgba(
                            style.background_color[0],
                            style.background_color[1],
                            style.background_color[2],
                            style.background_color[3] * opacity,
                        );
                    }
                    break;
                }
            }
            continue;
        }

        let opacity = style.shimmer_opacity(ph.animation_phase);
        let color = Color::linear_rgba(
            style.background_color[0],
            style.background_color[1],
            style.background_color[2],
            style.background_color[3] * opacity,
        );

        let min = ph.bounds.min.position;
        let max = ph.bounds.max.position;
        let cx = (min.x + max.x) * 0.5;
        let cy = (min.y + max.y) * 0.5;
        let half_w = ((max.x - min.x) * 0.5) as f32;
        let half_h = ((max.y - min.y) * 0.5) as f32;

        let z: f32 = -0.01;
        let positions = vec![
            [-half_w, -half_h, z],
            [half_w, -half_h, z],
            [half_w, half_h, z],
            [-half_w, half_h, z],
        ];
        let normals = vec![[0.0, 0.0, 1.0]; 4];
        let uvs = vec![[0.0, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]];

        let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, Default::default());
        mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
        mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
        mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
        mesh.insert_indices(Indices::U32(vec![0, 1, 2, 0, 2, 3]));

        let mesh_handle = meshes.add(mesh);

        let material_handle = materials.add(StandardMaterial {
            base_color: color,
            unlit: true,
            alpha_mode: AlphaMode::Blend,
            ..Default::default()
        });

        let tx = (cx - camera_origin.x) as f32;
        let ty = (cy - camera_origin.y) as f32;

        commands.spawn((
            Mesh3d(mesh_handle),
            MeshMaterial3d(material_handle),
            Transform::from_xyz(tx, ty, 0.0),
            Visibility::default(),
            PlaceholderEntity { tile_id: ph.tile },
        ));
    }
}