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 0000000000..df43731231 Binary files /dev/null and b/assets/map/elements/item/stomp_boots/stomp_boots.png differ 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 0000000000..81838154b8 Binary files /dev/null and b/assets/map/elements/item/stomp_boots/stomp_boots_icon.png differ diff --git a/assets/map/levels/level1.map.yaml b/assets/map/levels/level1.map.yaml index 0f8dc36eaa..d95370570b 100644 --- a/assets/map/levels/level1.map.yaml +++ b/assets/map/levels/level1.map.yaml @@ -1218,6 +1218,10 @@ layers: - id: items kind: !element elements: + - pos: + - 365 + - 110 + element: ../elements/item/stomp_boots/stomp_boots.element.yaml - pos: - 536.0 - 309.5 @@ -1270,14 +1274,6 @@ layers: - 171.5714 - 409.5 element: ../elements/item/kick_bomb/kick_bomb.element.yaml - - pos: - - 712.0 - - 393.5 - element: ../elements/item/mine/mine.element.yaml - - pos: - - 720.9998 - - 121.5 - element: ../elements/item/cannon/cannon.element.yaml - id: spawners kind: !element elements: diff --git a/src/animation.rs b/src/animation.rs index 1519f1b43f..e61c02b13e 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 + 1) 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..65a7605bb5 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)); @@ -498,6 +512,22 @@ impl AssetLoader for MapElementMetaLoader { *handle = sound_handle.typed(); } } + BuiltinElementKind::StompBoots { + map_icon, + map_icon_handle: map_icon_atlas, + player_decoration, + player_decoration_handle, + .. + } => { + 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/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, 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) } }