From f50119db3c76bb61b8bc92cac610ba714228a774 Mon Sep 17 00:00:00 2001 From: Zicklag Date: Mon, 5 Dec 2022 14:45:22 -0600 Subject: [PATCH 1/2] Add Stackable Animated Sprites and Decorations --- src/animation.rs | 73 ++++++++++++++++++++++++++++++++++++++++-- src/assets.rs | 16 ++++++++- src/metadata/player.rs | 5 +++ 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/animation.rs b/src/animation.rs index 1519f1b43f..4b209adb3c 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -56,6 +56,7 @@ pub struct AnimatedSprite { pub repeat: bool, pub fps: f32, pub timer: f32, + pub stacked_atlases: Vec>, } impl Clone for AnimatedSprite { @@ -69,6 +70,7 @@ impl Clone for AnimatedSprite { repeat: self.repeat, fps: self.fps, atlas: self.atlas.clone_weak(), + stacked_atlases: self.stacked_atlases.clone(), timer: self.timer, } } @@ -90,10 +92,25 @@ pub struct AnimationBank { pub last_animation: String, } -fn animate_sprites(mut animated_sprites: Query<(&mut AnimatedSprite, &mut TextureAtlasSprite)>) { - for (mut animated_sprite, mut atlas_sprite) in &mut animated_sprites { +#[derive(Component, Clone, Copy, Default)] +pub struct StackedAtlas; + +fn animate_sprites( + mut commands: Commands, + mut animated_sprites: Query<(Entity, &mut AnimatedSprite), With>, + mut texture_atlases: Query<( + &mut TextureAtlasSprite, + &Handle, + Option<&Parent>, + Option<&StackedAtlas>, + )>, +) { + for (sprite_ent, mut animated_sprite) in &mut animated_sprites { + let (mut atlas_sprite, ..) = texture_atlases.get_mut(sprite_ent).unwrap(); + animated_sprite.timer += 1.0 / crate::FPS as f32; atlas_sprite.flip_x = animated_sprite.flip_x; + atlas_sprite.flip_y = animated_sprite.flip_y; if animated_sprite.timer > 1.0 / animated_sprite.fps { animated_sprite.timer = 0.0; @@ -110,7 +127,57 @@ fn animate_sprites(mut animated_sprites: Query<(&mut AnimatedSprite, &mut Textur animated_sprite.index %= (animated_sprite.end - animated_sprite.start).max(1); } - atlas_sprite.index = animated_sprite.start + animated_sprite.index; + let sprite_index = animated_sprite.start + animated_sprite.index; + atlas_sprite.index = sprite_index; + + // Now we need to handle all the decorations + let mut pending_stacks = animated_sprite + .stacked_atlases + .iter() + .map(Some) + .collect::>(); + + // If there are already spawned images for these stacked atlases, then update them + for (mut sprite, atlas_handle, ..) in + texture_atlases + .iter_mut() + .filter(|(_, _, parent, stacked)| { + parent.map(|x| x.get()) == Some(sprite_ent) && stacked.is_some() + }) + { + // Take this sprite out of the list of pending stacks + for item in &mut pending_stacks { + if *item == Some(atlas_handle) { + *item = None; + } + } + sprite.flip_x = animated_sprite.flip_x; + sprite.flip_y = animated_sprite.flip_y; + sprite.index = sprite_index; + } + + // For any stacked sprite that we haven't done yet + const STACK_Z_DIFF: f32 = 0.01; + for (i, atlas) in pending_stacks.into_iter().enumerate() { + if let Some(atlas) = atlas { + let stack_ent = commands + .spawn() + .insert_bundle(SpriteSheetBundle { + texture_atlas: atlas.clone_weak(), + sprite: TextureAtlasSprite { + index: sprite_index, + flip_x: animated_sprite.flip_x, + flip_y: animated_sprite.flip_y, + ..default() + }, + transform: Transform::from_xyz(0.0, 0.0, i as f32 * STACK_Z_DIFF), + ..default() + }) + .insert(StackedAtlas) + .id(); + commands.entity(sprite_ent).add_child(stack_ent); + } + } } } diff --git a/src/assets.rs b/src/assets.rs index 362fb47325..4002d86403 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -237,7 +237,7 @@ impl AssetLoader for PlayerMetaLoader { load_context: &'a mut bevy::asset::LoadContext, ) -> bevy::utils::BoxedFuture<'a, Result<(), anyhow::Error>> { Box::pin(async move { - let self_path = load_context.path(); + let self_path = &load_context.path().to_owned(); let mut dependencies = Vec::new(); let mut meta: PlayerMeta = if self_path.extension() == Some(OsStr::new("json")) { serde_json::from_slice(bytes)? @@ -271,6 +271,20 @@ impl AssetLoader for PlayerMetaLoader { .with_dependency(atlas_path.clone()), ); meta.spritesheet.atlas_handle = AssetHandle::new(atlas_path, atlas_handle); + for (i, decoration) in meta.spritesheet.decorations.iter().enumerate() { + let (path, handle) = get_relative_asset(load_context, self_path, decoration); + dependencies.push(path); + let atlas_handle = load_context.set_labeled_asset( + &format!("decoration_atlas_{}", i), + LoadedAsset::new(TextureAtlas::from_grid( + handle.typed(), + meta.spritesheet.tile_size.as_vec2(), + meta.spritesheet.columns, + meta.spritesheet.rows, + )), + ); + meta.spritesheet.decoration_handles.push(atlas_handle); + } load_context.set_default_asset(LoadedAsset::new(meta).with_dependencies(dependencies)); diff --git a/src/metadata/player.rs b/src/metadata/player.rs index 8baf54e177..24e562a9f2 100644 --- a/src/metadata/player.rs +++ b/src/metadata/player.rs @@ -38,6 +38,10 @@ pub struct PlayerSpritesheetMeta { pub rows: usize, pub animation_fps: f32, pub animations: HashMap, + #[serde(default)] + pub decorations: Vec, + #[serde(skip)] + pub decoration_handles: Vec>, } #[derive(Reflect, Deserialize, Clone, Debug, Default)] @@ -77,6 +81,7 @@ impl PlayerSpritesheetMeta { start: clip.frames.start, end: clip.frames.end, atlas: self.atlas_handle.inner.clone_weak(), + stacked_atlases: self.decoration_handles.clone(), flip_x: false, flip_y: false, repeat: clip.repeat, From 90d1762ee7e3841859cd43acbebb029044fb10e7 Mon Sep 17 00:00:00 2001 From: Zicklag Date: Mon, 5 Dec 2022 17:02:50 -0600 Subject: [PATCH 2/2] Implement Stomp Boots --- .../item/stomp_boots/stomp_boots.atlas.yaml | 4 + .../item/stomp_boots/stomp_boots.element.yaml | 9 + .../elements/item/stomp_boots/stomp_boots.png | Bin 0 -> 12762 bytes .../stomp_boots/stomp_boots_icon.atlas.yaml | 4 + .../item/stomp_boots/stomp_boots_icon.png | Bin 0 -> 460 bytes assets/map/levels/level1.map.yaml | 12 +- src/animation.rs | 2 +- src/assets.rs | 16 ++ src/map/elements.rs | 2 + src/map/elements/player_spawner.rs | 5 +- src/map/elements/stomp_boots.rs | 245 ++++++++++++++++++ src/metadata/map.rs | 14 + src/physics/collisions.rs | 12 +- 13 files changed, 314 insertions(+), 11 deletions(-) create mode 100644 assets/map/elements/item/stomp_boots/stomp_boots.atlas.yaml create mode 100644 assets/map/elements/item/stomp_boots/stomp_boots.element.yaml create mode 100644 assets/map/elements/item/stomp_boots/stomp_boots.png create mode 100644 assets/map/elements/item/stomp_boots/stomp_boots_icon.atlas.yaml create mode 100644 assets/map/elements/item/stomp_boots/stomp_boots_icon.png create mode 100644 src/map/elements/stomp_boots.rs diff --git a/assets/map/elements/item/stomp_boots/stomp_boots.atlas.yaml b/assets/map/elements/item/stomp_boots/stomp_boots.atlas.yaml new file mode 100644 index 0000000000..3d87ce1406 --- /dev/null +++ b/assets/map/elements/item/stomp_boots/stomp_boots.atlas.yaml @@ -0,0 +1,4 @@ +image: ./stomp_boots.png +tile_size: [96, 80] +rows: 7 +columns: 14 diff --git a/assets/map/elements/item/stomp_boots/stomp_boots.element.yaml b/assets/map/elements/item/stomp_boots/stomp_boots.element.yaml new file mode 100644 index 0000000000..17e9e97d05 --- /dev/null +++ b/assets/map/elements/item/stomp_boots/stomp_boots.element.yaml @@ -0,0 +1,9 @@ +name: Sword +category: Weapons +builtin: !StompBoots + map_icon: ./stomp_boots_icon.atlas.yaml + player_decoration: ./stomp_boots.atlas.yaml + + body_size: [32, 18] + body_offset: [0, 0] + grab_offset: [-8, -4] \ No newline at end of file diff --git a/assets/map/elements/item/stomp_boots/stomp_boots.png b/assets/map/elements/item/stomp_boots/stomp_boots.png new file mode 100644 index 0000000000000000000000000000000000000000..df43731231f0d30c811361dca7b8b2fc14a1a715 GIT binary patch literal 12762 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 literal 0 HcmV?d00001 diff --git a/assets/map/elements/item/stomp_boots/stomp_boots_icon.atlas.yaml b/assets/map/elements/item/stomp_boots/stomp_boots_icon.atlas.yaml new file mode 100644 index 0000000000..36557efaf5 --- /dev/null +++ b/assets/map/elements/item/stomp_boots/stomp_boots_icon.atlas.yaml @@ -0,0 +1,4 @@ +image: ./stomp_boots_icon.png +tile_size: [31, 18] +rows: 1 +columns: 1 diff --git a/assets/map/elements/item/stomp_boots/stomp_boots_icon.png b/assets/map/elements/item/stomp_boots/stomp_boots_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..81838154b8f2e5f3f1b137faa4b2aa2cffd45151 GIT binary patch literal 460 zcmV;-0WM0004#NklK-oo|L*k3DulqbWvvay7^c%H09921D0OXfYi-E$oGikC-|2JPuM&b7BBP=GP5a&S1^E|~EV`{@W z2V9`-i^(W#cl+Af%iAZ*l=mJ$_0BPBqjtO7Gw460nC)&)RaHm#92KhuIN4lM zTbAWvqj$$Z#K}b33}e+NnM-Q3ENi7!YB-)zmSutrNPx*CpaEQ0Y5_GI&(e2V>m$HK zqAJED;3BO6#d@P6ND`jr9mWH__o^t00Hms=M0b=-ux8$s+IYCu`md*> { + for (atlas, atlas_handle) in [ + (map_icon, map_icon_atlas), + (player_decoration, player_decoration_handle), + ] { + let (path, handle) = get_relative_asset(load_context, self_path, atlas); + *atlas_handle = handle.typed(); + dependencies.push(path); + } + } } // Load preloaded assets diff --git a/src/map/elements.rs b/src/map/elements.rs index 6b1aa1a4c3..53b5f98d3f 100644 --- a/src/map/elements.rs +++ b/src/map/elements.rs @@ -21,6 +21,7 @@ pub mod sproinger; pub mod crate_item; pub mod grenade; pub mod mine; +pub mod stomp_boots; pub mod sword; pub struct MapElementsPlugin; @@ -33,6 +34,7 @@ impl Plugin for MapElementsPlugin { .add_plugin(player_spawner::PlayerSpawnerPlugin) .add_plugin(sproinger::SproingerPlugin) .add_plugin(mine::MinePlugin) + .add_plugin(stomp_boots::StompBootsPlugin) .add_plugin(sword::SwordPlugin); } } diff --git a/src/map/elements/player_spawner.rs b/src/map/elements/player_spawner.rs index d130c826e6..2f5c3b2c26 100644 --- a/src/map/elements/player_spawner.rs +++ b/src/map/elements/player_spawner.rs @@ -64,10 +64,13 @@ fn pre_update_in_game( break; }; + let mut spawn_location = spawn_point.translation; + spawn_location.z += i as f32 * 0.1; + commands .spawn() .insert(PlayerIdx(i)) - .insert(**spawn_point) + .insert(Transform::from_translation(spawn_location)) .insert(Rollback::new(ridp.next_id())); } } diff --git a/src/map/elements/stomp_boots.rs b/src/map/elements/stomp_boots.rs new file mode 100644 index 0000000000..6759e499e1 --- /dev/null +++ b/src/map/elements/stomp_boots.rs @@ -0,0 +1,245 @@ +//! The crate item. +//! +//! This module is inconsistently named with the rest of the modules ( i.e. has an `_item` suffix ) +//! because `crate` is a Rust keyword. + +use std::cmp::Ordering; + +use crate::{physics::collisions::Rect, player::PlayerKillCommand}; + +use super::*; + +pub struct StompBootsPlugin; + +impl Plugin for StompBootsPlugin { + fn build(&self, app: &mut App) { + app.add_rollback_system(RollbackStage::PreUpdate, pre_update_in_game) + .add_rollback_system(RollbackStage::Update, update_idle_stomp_boots) + .add_rollback_system(RollbackStage::Update, kill_stomped_players); + } +} + +/// Marker component added to things ( presumably players, but not necessarily! ) that are wearing +/// stomp boots +#[derive(Debug, Clone, Copy, Default, Component)] +pub struct WearingStompBoots; + +#[derive(Debug, Clone, Copy, Component)] +pub struct IdleStompBoots { + pub spawner: Entity, +} + +fn pre_update_in_game( + mut commands: Commands, + non_hydrated_map_elements: Query< + (Entity, &Sort, &Handle, &Transform), + Without, + >, + mut ridp: ResMut, + element_assets: ResMut>, +) { + // Hydrate any newly-spawned crates + let mut elements = non_hydrated_map_elements.iter().collect::>(); + elements.sort_by_key(|x| x.1); + for (entity, _sort, map_element_handle, transform) in elements { + let map_element = element_assets.get(map_element_handle).unwrap(); + if let BuiltinElementKind::StompBoots { + body_size, + body_offset, + map_icon_handle, + .. + } = &map_element.builtin + { + commands.entity(entity).insert(MapElementHydrated); + + commands + .spawn() + .insert(Rollback::new(ridp.next_id())) + .insert(Item { + script: "core:stomp_boots".into(), + }) + .insert(EntityName("Item: Stomp Boots".into())) + .insert(IdleStompBoots { spawner: entity }) + .insert(AnimatedSprite { + start: 0, + end: 0, + atlas: map_icon_handle.clone_weak(), + repeat: false, + ..default() + }) + .insert(map_element_handle.clone_weak()) + .insert_bundle(VisibilityBundle::default()) + .insert_bundle(TransformBundle { + local: *transform, + ..default() + }) + .insert(KinematicBody { + size: *body_size, + offset: *body_offset, + gravity: 1.0, + has_mass: true, + has_friction: true, + ..default() + }); + } + } +} + +fn update_idle_stomp_boots( + mut commands: Commands, + mut players: Query<(&mut AnimatedSprite, &Transform, &KinematicBody), With>, + mut grenades: Query< + ( + &Rollback, + Entity, + &IdleStompBoots, + &mut Transform, + &mut AnimatedSprite, + &mut KinematicBody, + &Handle, + Option<&Parent>, + Option<&ItemUsed>, + Option<&ItemDropped>, + ), + Without, + >, + element_assets: ResMut>, +) { + let mut items = grenades.iter_mut().collect::>(); + items.sort_by_key(|x| x.0.id()); + for ( + _, + item_ent, + boots, + mut transform, + mut sprite, + mut body, + meta_handle, + parent, + used, + dropped, + ) in items + { + let meta = element_assets.get(meta_handle).unwrap(); + let BuiltinElementKind::StompBoots { + grab_offset, + player_decoration_handle, + .. + } = &meta.builtin else { + unreachable!(); + }; + + // If the item is being held + if let Some(parent) = parent { + let (mut player_sprite, ..) = + players.get_mut(parent.get()).expect("Parent is not player"); + + // Deactivate items while held + body.is_deactivated = true; + + // Flip the sprite to match the player orientation + let flip = player_sprite.flip_x; + sprite.flip_x = flip; + let flip_factor = if flip { -1.0 } else { 1.0 }; + transform.translation.x = grab_offset.x * flip_factor; + transform.translation.y = grab_offset.y; + transform.translation.z = 1.0; + + // If the item is being used + if used.is_some() { + // Use up the boots + commands.entity(item_ent).despawn(); + // This will make the boots respawn at their spawn point + commands + .entity(boots.spawner) + .remove::(); + commands.entity(parent.get()).insert(WearingStompBoots); + + // Have the player wear the boots + if !player_sprite + .stacked_atlases + .contains(player_decoration_handle) + { + player_sprite + .stacked_atlases + .push(player_decoration_handle.clone_weak()); + } + } + } + + // If the item is dropped + if let Some(dropped) = dropped { + commands.entity(item_ent).remove::(); + let (player_sprite, player_transform, player_body) = + players.get(dropped.player).expect("Parent is not a player"); + + // Re-activate physics + body.is_deactivated = false; + + // Put sword in rest position + sprite.start = 0; + sprite.end = 0; + body.velocity = player_body.velocity; + body.is_spawning = true; + + let horizontal_flip_factor = if player_sprite.flip_x { + Vec2::new(-1.0, 1.0) + } else { + Vec2::ONE + }; + + // Drop item at player position + transform.translation = + player_transform.translation + (*grab_offset * horizontal_flip_factor).extend(1.0); + } + } +} + +fn kill_stomped_players( + mut commands: Commands, + players: Query>, + stompers: Query<(Entity, &KinematicBody), With>, + collision_world: CollisionWorld, +) { + // For all players wearing stomp boots + for (stomper, stomper_body) in &stompers { + let collisions = collision_world.actor_collisions(stomper); + + // Require that the stomper be moving down to stomp + if stomper_body.velocity.y.partial_cmp(&0.0) != Some(Ordering::Less) { + continue; + } + + // For every collision + for colliding_ent in collisions { + // If that collision is with a player + if players.contains(colliding_ent) { + let mut stomper_rect = collision_world.get_collider(stomper).rect(); + let mut player_rect = collision_world.get_collider(colliding_ent).rect(); + + // Modify the stomper rect to represent the feet of the stomper, and modify the + // player rect to represent the head of the player. + // + // TODO: We may want better stomp logic than + stomper_rect = Rect::new( + stomper_rect.min().x, + stomper_rect.min().y, + stomper_rect.width(), + stomper_rect.height() / 2.0, + ); + + let player_height = player_rect.height() / 5.0; + player_rect = Rect::new( + player_rect.min().x, + player_rect.max().y - player_height, + player_rect.width(), + player_height, + ); + + if stomper_rect.overlaps(&player_rect) { + commands.add(PlayerKillCommand::new(colliding_ent)); + } + } + } + } +} diff --git a/src/metadata/map.rs b/src/metadata/map.rs index 1b709403a5..7c6c3abc5b 100644 --- a/src/metadata/map.rs +++ b/src/metadata/map.rs @@ -319,4 +319,18 @@ pub enum BuiltinElementKind { body_offset: Vec2, grab_offset: Vec2, }, + + StompBoots { + map_icon: String, + #[serde(skip)] + map_icon_handle: Handle, + + player_decoration: String, + #[serde(skip)] + player_decoration_handle: Handle, + + body_size: Vec2, + body_offset: Vec2, + grab_offset: Vec2, + }, } diff --git a/src/physics/collisions.rs b/src/physics/collisions.rs index 775e2c3be8..f6db224912 100644 --- a/src/physics/collisions.rs +++ b/src/physics/collisions.rs @@ -29,6 +29,16 @@ impl Rect { Self { min, max } } + #[inline] + pub fn width(&self) -> f32 { + self.max.x - self.min.x + } + + #[inline] + pub fn height(&self) -> f32 { + self.max.y - self.min.y + } + #[inline] pub fn left(&self) -> f32 { self.min.x @@ -144,7 +154,7 @@ pub struct Collider { } impl Collider { - fn rect(&self) -> Rect { + pub fn rect(&self) -> Rect { Rect::new(self.pos.x, self.pos.y, self.width, self.height) } }