diff --git a/assets/sfx/abilities/crossbow.ogg b/assets/sfx/abilities/crossbow.ogg new file mode 100644 index 0000000..f9e9fb5 Binary files /dev/null and b/assets/sfx/abilities/crossbow.ogg differ diff --git a/src/abilities/arrow.rs b/src/abilities/arrow.rs new file mode 100644 index 0000000..d8e48c0 --- /dev/null +++ b/src/abilities/arrow.rs @@ -0,0 +1,111 @@ +use super::TriggerArrow; +use crate::{ + GameState, billboards::Billboard, global_observer, hitpoints::Hit, loading_assets::GameAssets, + physics_layers::GameLayer, sounds::PlaySound, utils::sprite_3d_animation::AnimationTimer, +}; +use avian3d::prelude::*; +use bevy::{pbr::NotShadowCaster, prelude::*}; +use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams}; +use std::f32::consts::PI; + +#[derive(Component)] +struct ArrowProjectile; + +#[derive(Resource)] +struct ShotAssets { + image: Handle, + layout: Handle, +} + +pub fn plugin(app: &mut App) { + app.add_systems(OnEnter(GameState::Playing), setup); + app.add_systems(Update, update.run_if(in_state(GameState::Playing))); + + global_observer!(app, on_trigger_arrow); +} + +fn setup(mut commands: Commands, assets: Res, mut sprite_params: Sprite3dParams) { + let layout = TextureAtlasLayout::from_grid(UVec2::splat(256), 7, 6, None, None); + let texture_atlas_layout = sprite_params.atlas_layouts.add(layout); + + commands.insert_resource(ShotAssets { + image: assets.impact_atlas.clone(), + layout: texture_atlas_layout, + }); +} + +fn on_trigger_arrow( + trigger: Trigger, + mut commands: Commands, + query_transform: Query<&Transform>, +) { + let state = trigger.0; + + commands.trigger(PlaySound::Crossbow); + + let rotation = if let Some(target) = state.target { + let t = query_transform + .get(target) + .expect("target must have transform"); + Transform::from_translation(state.pos) + .looking_at(t.translation, Vec3::Y) + .rotation + } else { + state.rot.mul_quat(Quat::from_rotation_y(PI)) + }; + + let mut t = Transform::from_translation(state.pos).with_rotation(rotation); + t.translation += (t.forward().as_vec3() * 2.) + (Vec3::Y * 0.6); + + commands.spawn((Name::new("projectile-arrow"), ArrowProjectile, t)); +} + +fn update( + mut cmds: Commands, + query: Query<(Entity, &Transform), With>, + spatial_query: SpatialQuery, + assets: Res, + mut sprite_params: Sprite3dParams, +) { + for (e, t) in query.iter() { + let filter = SpatialQueryFilter::from_mask(LayerMask( + GameLayer::Level.to_bits() | GameLayer::Npc.to_bits(), + )); + + if let Some(first_hit) = spatial_query.cast_shape( + &Collider::sphere(0.5), + t.translation, + t.rotation, + t.forward(), + &ShapeCastConfig::from_max_distance(80.), + &filter, + ) { + cmds.entity(first_hit.entity).trigger(Hit { damage: 50 }); + + cmds.spawn( + Sprite3dBuilder { + image: assets.image.clone(), + pixels_per_metre: 128., + alpha_mode: AlphaMode::Blend, + unlit: true, + ..default() + } + .bundle_with_atlas( + &mut sprite_params, + TextureAtlas { + layout: assets.layout.clone(), + index: 0, + }, + ), + ) + .insert(( + Billboard, + Transform::from_translation(first_hit.point1), + NotShadowCaster, + AnimationTimer::new(Timer::from_seconds(0.005, TimerMode::Repeating)), + )); + } + + cmds.entity(e).despawn(); + } +} diff --git a/src/abilities/mod.rs b/src/abilities/mod.rs index 6a79a3d..44107dc 100644 --- a/src/abilities/mod.rs +++ b/src/abilities/mod.rs @@ -1,3 +1,4 @@ +mod arrow; mod gun; mod thrown; @@ -73,11 +74,14 @@ impl TriggerData { #[derive(Event, Reflect)] pub struct TriggerGun(pub TriggerData); #[derive(Event, Reflect)] +pub struct TriggerArrow(pub TriggerData); +#[derive(Event, Reflect)] pub struct TriggerThrow(pub TriggerData); pub fn plugin(app: &mut App) { app.add_plugins(gun::plugin); app.add_plugins(thrown::plugin); + app.add_plugins(arrow::plugin); app.add_systems(Update, enemy_hit.run_if(in_state(GameState::Playing))); @@ -155,7 +159,8 @@ fn on_trigger_state( match ability { HeadAbility::Thrown => commands.trigger(TriggerThrow(trigger_state)), HeadAbility::Gun => commands.trigger(TriggerGun(trigger_state)), - _ => (), + HeadAbility::Arrow => commands.trigger(TriggerArrow(trigger_state)), + HeadAbility::None => (), }; } } diff --git a/src/loading_assets.rs b/src/loading_assets.rs index e312741..abd7043 100644 --- a/src/loading_assets.rs +++ b/src/loading_assets.rs @@ -15,6 +15,8 @@ pub struct AudioAssets { pub key_collect: Handle, #[asset(path = "sfx/abilities/gun.ogg")] pub gun: Handle, + #[asset(path = "sfx/abilities/crossbow.ogg")] + pub crossbow: Handle, #[asset(path = "sfx/effects/gate.ogg")] pub gate: Handle, #[asset(path = "sfx/effects/cash.ogg")] diff --git a/src/sounds.rs b/src/sounds.rs index 9db5c71..31e85b8 100644 --- a/src/sounds.rs +++ b/src/sounds.rs @@ -14,6 +14,7 @@ pub enum PlaySound { Invalid, Reloaded, CashHeal, + Crossbow, Backpack { open: bool }, Head(String), } @@ -42,6 +43,7 @@ fn on_spawn_sounds( } PlaySound::KeyCollect => assets.key_collect.clone(), PlaySound::Gun => assets.gun.clone(), + PlaySound::Crossbow => assets.crossbow.clone(), PlaySound::Gate => assets.gate.clone(), PlaySound::CashCollect => assets.cash.clone(), PlaySound::Selection => assets.selection.clone(),