From 609b11ca81103c51bf9867df212525d5aa7539e0 Mon Sep 17 00:00:00 2001 From: Max Whitehead <35712032+MaxCWhitehead@users.noreply.github.com> Date: Sun, 14 Apr 2024 16:58:05 -0700 Subject: [PATCH] fix(kick_bomb): Add spawn kickbomb command to allow spawning from gameplay code without interfering with map hydration (#968) `commands.add(KickBombCommand::spawn_kick_bomb(Some(entity), transform, kick_bomb_meta_handle)` may be used to spawn a kick bomb. - If entity is passed in it will initialize kick bomb components on this entity (mostly to support hydration from map elements), otherwise None will make it create an entity for you. - The handle is untyped handle (can use `handle.untyped()` to convert to this), it supports `Handle` or `Handle` automatically. This command adds a `KickBombHandle` containing `Handle`, this should be used to get meta for entities. The kick bomb systems now use this instead of `Handle`. Gameplay systems were also updated such that usages of components `MapElementHydrated` and `DehydratedOutOfBounds` (things specific to map spawned kickbombs) are optionally used, meaning these systems still run for gameplay spawned kickbombs missing these components. Under the hood, `try_cast_meta_handle` helper function was added to help convert an untyped `Handle` or `Handle` to `Handle`. This function is generic and may be used for other gameplay items that we refactor to add spawn commands to separate item spawn logic from map spawning. I tested this and the map spawned kickbombs will still respawn after use, but the gameplay spawned ones do not. --- src/core/elements/kick_bomb.rs | 224 +++++++++++++++++++-------------- src/core/utils.rs | 2 + src/core/utils/metadata.rs | 56 +++++++++ 3 files changed, 190 insertions(+), 92 deletions(-) create mode 100644 src/core/utils/metadata.rs diff --git a/src/core/elements/kick_bomb.rs b/src/core/elements/kick_bomb.rs index 232364ae7b..1e9ee0ce6f 100644 --- a/src/core/elements/kick_bomb.rs +++ b/src/core/elements/kick_bomb.rs @@ -55,86 +55,142 @@ pub struct LitKickBomb { kicks: u32, } -fn hydrate( - game_meta: Root, - mut items: CompMut, - mut item_throws: CompMut, - mut item_grabs: CompMut, - mut entities: ResMutInit, - mut bodies: CompMut, - mut transforms: CompMut, - mut idle_bombs: CompMut, - mut atlas_sprites: CompMut, - assets: Res, - mut hydrated: CompMut, - mut element_handles: CompMut, - mut animated_sprites: CompMut, - mut respawn_points: CompMut, - mut spawner_manager: SpawnerManager, -) { - let mut not_hydrated_bitset = hydrated.bitset().clone(); - not_hydrated_bitset.bit_not(); - not_hydrated_bitset.bit_and(element_handles.bitset()); - - let spawner_entities = entities - .iter_with_bitset(¬_hydrated_bitset) - .collect::>(); - - for spawner_ent in spawner_entities { - let transform = *transforms.get(spawner_ent).unwrap(); - let element_handle = *element_handles.get(spawner_ent).unwrap(); - let element_meta = assets.get(element_handle.0); - - if let Ok(KickBombMeta { - atlas, - fin_anim, - grab_offset, - body_diameter, - can_rotate, - bounciness, - throw_velocity, - angular_velocity, - .. - }) = assets.get(element_meta.data).try_cast_ref() - { - hydrated.insert(spawner_ent, MapElementHydrated); - - let entity = entities.create(); - hydrated.insert(entity, MapElementHydrated); - element_handles.insert(entity, element_handle); +/// Component containing the kick bombs's metadata handle. +#[derive(Deref, DerefMut, HasSchema, Default, Clone)] +#[repr(C)] +pub struct KickBombHandle(pub Handle); + +/// Commands for KickBombs +#[derive(Clone, Debug)] +pub struct KickBombCommand; + +impl KickBombCommand { + /// Command for spawning a kick bomb. + /// If entity is provided, components are added to this entity, otherwise command spawns the entity + /// + /// `kick_bomb_handle` must cast to `Handle` or `Handle` where [`ElementMeta`] + /// contains handle that casts to `Handle`. + /// [`Handle::untyped`] should be used to convert to [`UntypedHandle`]. + pub fn spawn_kick_bomb( + entity: Option, + transform: Transform, + kick_bomb_meta_handle: UntypedHandle, + ) -> StaticSystem<(), ()> { + (move |game_meta: Root, + assets: Res, + mut animated_sprites: CompMut, + mut atlas_sprites: CompMut, + mut bodies: CompMut, + mut entities: ResMutInit, + mut idle_bombs: CompMut, + mut items: CompMut, + mut item_throws: CompMut, + mut item_grabs: CompMut, + mut kick_bomb_handles: CompMut, + mut transforms: CompMut| { + // Unwrap entity or spawn if existing entity was not provided. + let entity = entity.unwrap_or_else(|| entities.create()); + + // Try to use handle as Handle. + let kick_bomb_meta_handle = + match try_cast_meta_handle::(kick_bomb_meta_handle, &assets) { + Ok(handle) => handle, + Err(err) => { + error!("KickBombCommand::spawn_kick_bomb() failed: {err}"); + return; + } + }; + + let KickBombMeta { + atlas, + fin_anim, + grab_offset, + body_diameter, + can_rotate, + bounciness, + throw_velocity, + angular_velocity, + .. + } = *assets.get(kick_bomb_meta_handle); + kick_bomb_handles.insert(entity, KickBombHandle(kick_bomb_meta_handle)); items.insert(entity, Item); item_throws.insert( entity, - ItemThrow::strength(*throw_velocity).with_spin(*angular_velocity), + ItemThrow::strength(throw_velocity).with_spin(angular_velocity), ); item_grabs.insert( entity, ItemGrab { - fin_anim: *fin_anim, + fin_anim, sync_animation: false, - grab_offset: *grab_offset, + grab_offset, }, ); idle_bombs.insert(entity, IdleKickBomb); - atlas_sprites.insert(entity, AtlasSprite::new(*atlas)); - respawn_points.insert(entity, DehydrateOutOfBounds(spawner_ent)); + atlas_sprites.insert(entity, AtlasSprite::new(atlas)); transforms.insert(entity, transform); animated_sprites.insert(entity, default()); bodies.insert( entity, KinematicBody { shape: ColliderShape::Circle { - diameter: *body_diameter, + diameter: body_diameter, }, gravity: game_meta.core.physics.gravity, has_mass: true, has_friction: true, - can_rotate: *can_rotate, - bounciness: *bounciness, + can_rotate, + bounciness, ..default() }, ); - spawner_manager.create_spawner(spawner_ent, vec![entity]) + }) + .system() + } +} + +fn hydrate( + assets: Res, + mut entities: ResMutInit, + transforms: Comp, + mut hydrated: CompMut, + mut element_handles: CompMut, + mut respawn_points: CompMut, + mut spawner_manager: SpawnerManager, + mut commands: Commands, +) { + let mut not_hydrated_bitset = hydrated.bitset().clone(); + not_hydrated_bitset.bit_not(); + not_hydrated_bitset.bit_and(element_handles.bitset()); + + let spawner_entities = entities + .iter_with_bitset(¬_hydrated_bitset) + .collect::>(); + + for spawner_ent in spawner_entities { + let transform = *transforms.get(spawner_ent).unwrap(); + let element_handle = *element_handles.get(spawner_ent).unwrap(); + + hydrated.insert(spawner_ent, MapElementHydrated); + + let entity = entities.create(); + hydrated.insert(entity, MapElementHydrated); + element_handles.insert(entity, element_handle); + respawn_points.insert(entity, DehydrateOutOfBounds(spawner_ent)); + spawner_manager.create_spawner(spawner_ent, vec![entity]); + + // Check if spawner element handle is for kick bomb + let element_meta = assets.get(element_handle.0); + if assets + .get(element_meta.data) + .try_cast_ref::() + .is_ok() + { + commands.add(KickBombCommand::spawn_kick_bomb( + Some(entity), + transform, + element_meta.data.untyped(), + )); } } } @@ -144,18 +200,17 @@ fn update_idle_kick_bombs( mut commands: Commands, mut items_used: CompMut, mut audio_center: ResMut, - element_handles: Comp, + kick_bomb_handles: Comp, mut idle_bombs: CompMut, assets: Res, mut animated_sprites: CompMut, ) { - for (entity, (_kick_bomb, element_handle)) in - entities.iter_with((&mut idle_bombs, &element_handles)) + for (entity, (_kick_bomb, kick_bomb_handle)) in + entities.iter_with((&mut idle_bombs, &kick_bomb_handles)) { - let element_meta = assets.get(element_handle.0); + let kick_bomb_meta = assets.get(kick_bomb_handle.0); - let asset = assets.get(element_meta.data); - let Ok(KickBombMeta { + let KickBombMeta { fuse_sound, fuse_sound_volume, arm_delay, @@ -163,16 +218,10 @@ fn update_idle_kick_bombs( lit_frames, lit_fps, .. - }) = asset.try_cast_ref() - else { - unreachable!(); - }; - - let arm_delay = *arm_delay; - let fuse_time = *fuse_time; + } = *kick_bomb_meta; if items_used.remove(entity).is_some() { - audio_center.play_sound(*fuse_sound, *fuse_sound_volume); + audio_center.play_sound(fuse_sound, fuse_sound_volume); let animated_sprite = animated_sprites.get_mut(entity).unwrap(); animated_sprite.frames = lit_frames.clone(); animated_sprite.repeat = true; @@ -197,9 +246,8 @@ fn update_idle_kick_bombs( fn update_lit_kick_bombs( entities: Res, - element_handles: Comp, + kick_bomb_handles: Comp, assets: Res, - collision_world: CollisionWorld, player_indexes: Comp, mut audio_center: ResMut, @@ -215,12 +263,11 @@ fn update_lit_kick_bombs( spawners: Comp, invincibles: CompMut, ) { - for (entity, (kick_bomb, element_handle, spawner)) in - entities.iter_with((&mut lit_grenades, &element_handles, &spawners)) + for (entity, (kick_bomb, kick_bomb_handle, spawner)) in + entities.iter_with((&mut lit_grenades, &kick_bomb_handles, &Optional(&spawners))) { - let element_meta = assets.get(element_handle.0); - let asset = assets.get(element_meta.data); - let Ok(KickBombMeta { + let kick_bomb_meta = assets.get(kick_bomb_handle.0); + let KickBombMeta { explosion_sound, explosion_volume, kick_velocity, @@ -232,10 +279,7 @@ fn update_lit_kick_bombs( explosion_frames, explode_on_contact, .. - }) = asset.try_cast_ref() - else { - unreachable!(); - }; + } = *kick_bomb_meta; kick_bomb.fuse_time.tick(time.delta()); kick_bomb.arm_delay.tick(time.delta()); @@ -295,7 +339,7 @@ fn update_lit_kick_bombs( let player_standing_left = player_translation.x <= translation.x; if body.velocity.x == 0.0 { - body.velocity = *kick_velocity; + body.velocity = kick_velocity; if player_sprite.flip_x { body.velocity.x *= -1.0; } @@ -317,23 +361,19 @@ fn update_lit_kick_bombs( // If it's time to explode if should_explode { - audio_center.play_sound(*explosion_sound, *explosion_volume); + audio_center.play_sound(explosion_sound, explosion_volume); trauma_events.send(7.5); - // Cause the item to respawn by un-hydrating it's spawner. - hydrated.remove(**spawner); + if let Some(spawner) = spawner { + // Cause the item to respawn by un-hydrating it's spawner. + hydrated.remove(**spawner); + } + let mut explosion_transform = *transforms.get(entity).unwrap(); explosion_transform.translation.z = -10.0; // On top of almost everything explosion_transform.rotation = Quat::IDENTITY; - // Clone types for move into closure - let damage_region_size = *damage_region_size; - let damage_region_lifetime = *damage_region_lifetime; - let explosion_lifetime = *explosion_lifetime; - let explosion_atlas = *explosion_atlas; - let explosion_fps = *explosion_fps; - let explosion_frames = *explosion_frames; commands.add( move |mut entities: ResMutInit, mut transforms: CompMut, diff --git a/src/core/utils.rs b/src/core/utils.rs index 2bb709f29a..dd22626dcb 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -8,3 +8,5 @@ mod rect; pub use rect::*; mod macros; pub use macros::*; +mod metadata; +pub use metadata::*; diff --git a/src/core/utils/metadata.rs b/src/core/utils/metadata.rs new file mode 100644 index 0000000000..60dc7d16c3 --- /dev/null +++ b/src/core/utils/metadata.rs @@ -0,0 +1,56 @@ +//! Utilities for asset and meta data handling. + +use crate::prelude::*; + +/// Error type for failure to resolve [`UntypedHandle`] to metadata asset from `Handle` or `Handle` +/// (where `T` is metadata type). +#[derive(thiserror::Error, Debug, Clone)] +pub enum MetaHandleCastError { + #[error("UntypedHandle does not represent Handle<{0}> or Handle")] + BadHandleType(String), + #[error("UntypedHandle maps to Handle but ElementMeta inner handle does not cast to {0}")] + ElementDataMismatch(String), + #[error("Failed to retrieve asset for handle (required for cast type valdiation)")] + InvalidHandle, +} + +/// Try to get metadata handle from [`UntypedHandle`] that may represent direct handle to meta (`Handle`) +/// or `Handle` where [`ElementMeta`]'s data is castable to `Handle`. +/// +/// [`Handle::untyped`] can be used to convert to [`UntypedHandle`]. +/// +/// This is useful for code that wants to spawn an item and take UntypedHandle to allow either direct meta handle +/// or `Handle` as argument. +pub fn try_cast_meta_handle( + handle: UntypedHandle, + assets: &AssetServer, +) -> Result, MetaHandleCastError> { + // Get untyped asset from handle or error on failure + let asset = assets + .try_get_untyped(handle) + .ok_or(MetaHandleCastError::InvalidHandle)?; + + // If asset casts to T, return it as Handle + if asset.try_cast_ref::().is_ok() { + return Ok(handle.typed::()); + } + + // Check if handle type is ElementMeta + if let Ok(element_meta) = asset.try_cast_ref::() { + // Does element data cast to T? + if assets.get(element_meta.data).try_cast_ref::().is_ok() { + // Return ElementMeta's data as Handle + Ok(element_meta.data.untyped().typed()) + } else { + // ElementMeta data does not cast to T. + Err(MetaHandleCastError::ElementDataMismatch( + std::any::type_name::().to_string(), + )) + } + } else { + // UntypedHandle is neither Handle or Handle. + Err(MetaHandleCastError::BadHandleType( + std::any::type_name::().to_string(), + )) + } +}