station/crates/game_client/src/render.rs

298 lines
9.3 KiB
Rust
Raw Normal View History

//! 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},
map::MapLayout,
};
use crate::LocalPlayer;
pub struct RenderPlugin;
impl Plugin for RenderPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<LocalCharacter>()
.add_systems(Startup, setup)
.add_observer(spawn_map)
.add_systems(
Update,
(
find_local_character,
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<Entity>);
/// 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(
add: On<Add, WorldMap>,
mut commands: Commands,
maps: Query<&WorldMap>,
asset_server: Res<AssetServer>,
mut layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
let Ok(map) = maps.get(add.entity) else {
return;
};
let layout = match MapLayout::load(&game_core::assets_dir(), &map.path) {
Ok(layout) => layout,
Err(e) => {
error!("can't display map: {e}");
return;
}
};
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(add.entity)
.insert((Transform::default(), Visibility::default()));
for (layer_index, layer) in 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(add.entity),
Sprite {
image: image.clone(),
texture_atlas: Some(TextureAtlas {
layout: atlas.clone(),
index: cell.id as usize,
}),
flip_x: cell.flip_h,
flip_y: cell.flip_v,
custom_size: Some(Vec2::splat(TILE_SIZE)),
..default()
},
Transform::from_translation(grid_to_world(pos).extend(z)),
));
}
}
info!(
"map `{}` displayed ({}x{})",
map.path, layout.width, layout.height
);
}
fn find_local_character(
mut local: ResMut<LocalCharacter>,
player: Res<LocalPlayer>,
characters: Query<(Entity, &Character)>,
) {
if local.0.is_some_and(|e| characters.contains(e)) {
return;
}
local.0 = characters
.iter()
.find(|(_, c)| c.name == player.name)
.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<GameData>,
asset_server: Res<AssetServer>,
buildings: Query<
(Entity, &Building, &BuildingState, &GridPos, Has<Transform>),
Or<(Changed<BuildingState>, Changed<GridPos>, Without<Sprite>)>,
>,
) {
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);
entity.insert(sprite(
&asset_server,
def.sprite_for(state.variant.as_deref()),
TILE_SIZE,
));
let transform = Transform::from_translation(grid_to_world(*pos).extend(Z_BUILDING));
if has_transform {
entity.insert(transform);
} else {
entity.insert((transform, Visibility::default()));
}
}
}
#[derive(Component)]
struct StackLabel;
fn item_visuals(
mut commands: Commands,
data: Res<GameData>,
asset_server: Res<AssetServer>,
items: Query<(Entity, &GroundItem, &GridPos), Or<(Changed<GroundItem>, Changed<GridPos>)>>,
children: Query<&Children>,
labels: Query<(), With<StackLabel>>,
) {
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<AssetServer>,
local: Res<LocalCharacter>,
added: Query<(Entity, &Character, &GridPos), Added<Character>>,
mut facing: Query<(&Facing, &mut Sprite), (With<Character>, Changed<Facing>)>,
) {
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(grid_to_world(*pos).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;
}
}
/// Glides characters between tiles; teleports for large jumps (respawn, login).
fn move_characters(
time: Res<Time>,
mut characters: Query<(&GridPos, &mut Transform), With<Character>>,
) {
let speed = TILE_SIZE / game_server::movement::MOVE_DELAY * 1.2;
for (pos, mut transform) in &mut characters {
let target = grid_to_world(*pos);
let current = transform.translation.truncate();
let delta = target - current;
if delta.length() > TILE_SIZE * 3.0 {
transform.translation = target.extend(Z_CHARACTER);
} else {
let step = delta.clamp_length_max(speed * time.delta_secs());
transform.translation += step.extend(0.0);
}
}
}
fn update_marker(
local: Res<LocalCharacter>,
characters: Query<(&GridPos, &Facing)>,
mut marker: Query<(&mut Transform, &mut Visibility), With<FacingMarker>>,
) {
let Ok((mut transform, mut visibility)) = marker.single_mut() else {
return;
};
match local.0.and_then(|e| characters.get(e).ok()) {
Some((pos, facing)) => {
transform.translation = grid_to_world(pos.step(facing.0)).extend(Z_MARKER);
*visibility = Visibility::Visible;
}
None => *visibility = Visibility::Hidden,
}
}
fn follow_camera(
local: Res<LocalCharacter>,
time: Res<Time>,
targets: Query<&Transform, (With<Character>, Without<Camera2d>)>,
mut camera: Query<&mut Transform, With<Camera2d>>,
) {
let Some(target) = local.0.and_then(|e| targets.get(e).ok()) else {
return;
};
let Ok(mut camera) = camera.single_mut() else {
return;
};
let goal = target.translation.truncate().extend(camera.translation.z);
let t = 1.0 - (-8.0 * time.delta_secs()).exp();
camera.translation = camera.translation.lerp(goal, t);
}