ryot 0.2.2

MMORPG library based on the concepts of open tibia written in rust and bevy.
Documentation
use crate::bevy_ryot::map::MapTiles;
use crate::bevy_ryot::sprites::SPRITE_BASE_SIZE;
use crate::bevy_ryot::{AppearanceAssets, AppearanceGroup, GameObjectId};
use crate::position::TilePosition;
use crate::Layer;
use bevy::prelude::*;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::fmt::Display;

#[derive(Debug, Clone, Component, Copy, PartialEq, Serialize, Deserialize)]
pub struct Elevation {
    pub elevation: f32,
}

impl Default for Elevation {
    fn default() -> Self {
        Elevation { elevation: 0.0 }
    }
}

impl Display for Elevation {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "E:{}", self.elevation)
    }
}

impl Elevation {
    pub fn lerp(&self, other: &Elevation, fraction: f32) -> Elevation {
        Elevation {
            elevation: self.elevation.lerp(other.elevation, fraction),
        }
    }
}

type ElevationFilter = (
    With<GameObjectId>,
    Or<(
        Changed<GameObjectId>,
        Changed<Visibility>,
        Changed<TilePosition>,
    )>,
);

pub(crate) fn apply_elevation<C: AppearanceAssets>(
    appearance_assets: Res<C>,
    q_tile: Query<(&TilePosition, &Layer), ElevationFilter>,
    mut q_entities: Query<(&mut Elevation, &GameObjectId, Option<&Visibility>)>,
    map_tiles: Res<MapTiles<Entity>>,
) {
    let appearances = appearance_assets.prepared_appearances();
    for tile in q_tile
        .iter()
        .filter(|(_, layer)| matches!(layer, Layer::Bottom(_)))
        .map(|(pos, _)| *pos)
        .unique()
        .filter_map(|pos| map_tiles.get(&pos))
    {
        tile.into_iter()
            .filter(|(layer, _)| matches!(layer, Layer::Bottom(_)))
            .map(|(_, entity)| entity)
            .fold(0., |tile_elevation, entity| {
                let Ok((mut elevation, object_id, visibility)) = q_entities.get_mut(entity) else {
                    return tile_elevation;
                };
                let Some((group, id)) = object_id.as_group_and_id() else {
                    return tile_elevation;
                };

                let elevation_delta =
                    if visibility.cloned().unwrap_or_default() != Visibility::Hidden {
                        appearances
                            .get_for_group(group, id)
                            .cloned()
                            .and_then(|app| app.flags?.elevation?.height)
                            .unwrap_or(0) as f32
                            / SPRITE_BASE_SIZE.y as f32
                    } else {
                        0.
                    };

                elevation.elevation = match group {
                    AppearanceGroup::Object => tile_elevation,
                    AppearanceGroup::Outfit => tile_elevation,
                    AppearanceGroup::Effect => 0.,
                    AppearanceGroup::Missile => 0.,
                };

                tile_elevation + elevation_delta
            });
    }
}