//! World rendering: Tiled map layers, sprites for replicated entities and the camera. use bevy::prelude::*; use game_core::{ GameData, components::*, grid::{Dir, TILE_SIZE, grid_to_world, position_to_world}, }; use crate::session::SessionInfo; pub struct RenderPlugin; impl Plugin for RenderPlugin { fn build(&self, app: &mut App) { app.init_resource::() .add_systems(Startup, setup) .add_systems( Update, ( find_local_character, spawn_map, building_visuals, item_visuals, character_visuals, move_characters, update_marker, follow_camera, ) .chain(), ); } } const Z_BUILDING: f32 = 1.0; const Z_ITEM: f32 = 2.0; const Z_CHARACTER: f32 = 3.0; const Z_MARKER: f32 = 4.0; /// Our own character, once it has been replicated. #[derive(Resource, Default)] pub struct LocalCharacter(pub Option); /// Highlights the tile in front of the player (target of interactions). #[derive(Component)] struct FacingMarker; fn setup(mut commands: Commands) { commands.spawn(Camera2d); commands.spawn(( FacingMarker, Sprite::from_color(Color::srgba(1.0, 0.9, 0.2, 0.18), Vec2::splat(TILE_SIZE)), Transform::from_xyz(0.0, 0.0, Z_MARKER), Visibility::Hidden, )); } /// Builds sprites for every tile when the world map arrives. Tiles are children /// of the map entity, so they disappear with it (e.g. on disconnect). fn spawn_map( mut commands: Commands, maps: Query<(Entity, &WorldMap), Changed>, asset_server: Res, mut layouts: ResMut>, ) { for (entity, map) in &maps { let layout = &map.layout; commands.entity(entity).despawn_children(); let atlases: Vec<_> = layout .tilesets .iter() .map(|ts| { let rows = ts.tile_count.div_ceil(ts.columns.max(1)); let atlas = TextureAtlasLayout::from_grid( UVec2::new(ts.tile_width, ts.tile_height), ts.columns, rows, Some(UVec2::splat(ts.spacing)), Some(UVec2::splat(ts.margin)), ); (asset_server.load(ts.image.clone()), layouts.add(atlas)) }) .collect(); commands .entity(entity) .insert((Transform::default(), Visibility::default())); let default_layer = layout .default_tile .map(|cell| game_core::map::TileLayerData { name: "Default terrain".into(), solid: false, cells: vec![Some(cell); (layout.width * layout.height) as usize], }); for (layer_index, layer) in default_layer.iter().chain(layout.layers.iter()).enumerate() { let z = layer_index as f32 * 0.01; for (i, cell) in layer.cells.iter().enumerate() { let Some(cell) = cell else { continue }; let Some((image, atlas)) = atlases.get(cell.tileset) else { continue; }; let pos = GridPos::new( (i as u32 % layout.width) as i32, (i as u32 / layout.width) as i32, ); commands.spawn(( ChildOf(entity), Sprite { image: image.clone(), texture_atlas: Some(TextureAtlas { layout: atlas.clone(), index: cell.id as usize, }), flip_x: if cell.flip_d { !cell.flip_v } else { cell.flip_h }, flip_y: if cell.flip_d { cell.flip_h } else { cell.flip_v }, custom_size: Some(Vec2::splat(TILE_SIZE)), ..default() }, Transform::from_translation(grid_to_world(pos).extend(z)).with_rotation( Quat::from_rotation_z(if cell.flip_d { -std::f32::consts::FRAC_PI_2 } else { 0.0 }), ), )); } } info!( "map `{}` displayed ({}x{})", map.path, layout.width, layout.height ); } } fn find_local_character( mut local: ResMut, session: Res, characters: Query<(Entity, &Character)>, ) { if local.0.is_some_and(|e| characters.contains(e)) { return; } if session.character.is_empty() { local.0 = None; return; } local.0 = characters .iter() .find(|(_, c)| c.name == session.character) .map(|(e, _)| e); } fn sprite(asset_server: &AssetServer, path: &str, size: f32) -> Sprite { Sprite { image: asset_server.load(path.to_owned()), custom_size: Some(Vec2::splat(size)), ..default() } } fn building_visuals( mut commands: Commands, data: Res, asset_server: Res, buildings: Query< (Entity, &Building, &BuildingState, &GridPos, Has), Or<(Changed, Changed, Without)>, >, ) { for (entity, building, state, pos, has_transform) in &buildings { let Some(def) = data.buildings.get(&building.proto) else { continue; }; let mut entity = commands.entity(entity); let mut visual = sprite( &asset_server, def.sprite_for(state.variant.as_deref()), TILE_SIZE, ); visual.flip_x = if state.flip_d { !state.flip_v } else { state.flip_h }; visual.flip_y = if state.flip_d { state.flip_h } else { state.flip_v }; entity.insert(visual); let z = if state.layer == game_core::data::BuildingLayer::Floor { 0.5 } else { Z_BUILDING }; let transform = Transform::from_translation(grid_to_world(*pos).extend(z)).with_rotation( Quat::from_rotation_z(if state.flip_d { -std::f32::consts::FRAC_PI_2 } else { 0.0 }), ); if has_transform { entity.insert(transform); } else { entity.insert((transform, Visibility::default())); } } } #[derive(Component)] struct StackLabel; fn item_visuals( mut commands: Commands, data: Res, asset_server: Res, items: Query<(Entity, &GroundItem, &GridPos), Or<(Changed, Changed)>>, children: Query<&Children>, labels: Query<(), With>, ) { for (entity, item, pos) in &items { let Some(def) = data.items.get(&item.0.item) else { continue; }; for child in children.get(entity).into_iter().flatten() { if labels.contains(*child) { commands.entity(*child).despawn(); } } commands.entity(entity).insert(( sprite(&asset_server, &def.sprite, TILE_SIZE * 0.75), Transform::from_translation(grid_to_world(*pos).extend(Z_ITEM)), Visibility::default(), )); if item.0.count > 1 { commands.spawn(( ChildOf(entity), StackLabel, Text2d::new(item.0.count.to_string()), TextFont::from_font_size(10.0), Transform::from_xyz(9.0, -9.0, 0.1), )); } } } fn character_visuals( mut commands: Commands, asset_server: Res, local: Res, added: Query<(Entity, &Character, &Position), Added>, mut facing: Query<(&Facing, &mut Sprite), (With, Changed)>, ) { for (entity, character, pos) in &added { let mut sprite = sprite(&asset_server, "sprites/characters/player.png", TILE_SIZE); if local.0 != Some(entity) { sprite.color = Color::srgb(0.8, 0.9, 1.0); } commands.entity(entity).insert(( sprite, Transform::from_translation(position_to_world(pos.0).extend(Z_CHARACTER)), Visibility::default(), children![( Text2d::new(character.name.clone()), TextFont::from_font_size(11.0), Transform::from_xyz(0.0, TILE_SIZE * 0.7, 0.1), )], )); } for (facing, mut sprite) in &mut facing { sprite.flip_x = facing.0 == Dir::West; } } /// Smooths replicated positions, which arrive at the server tick rate; teleports for large /// jumps (respawn, login). fn move_characters( time: Res