diff --git a/pumpkin/src/entity/ai/goal/blaze_attack.rs b/pumpkin/src/entity/ai/goal/blaze_attack.rs new file mode 100644 index 000000000..dba54c5c1 --- /dev/null +++ b/pumpkin/src/entity/ai/goal/blaze_attack.rs @@ -0,0 +1,220 @@ +use pumpkin_protocol::java::client::play::CWorldEvent; +use pumpkin_util::math::vector3::Vector3; +use rand::RngExt; +use std::sync::Arc; + +use crate::entity::{ + Entity, + ai::goal::{Controls, Goal, GoalFuture}, + mob::Mob, + mob::blaze::BlazeEntity, + projectile::small_fireball::SmallFireballEntity, +}; + +pub struct BlazeShootFireballGoal { + blaze: std::sync::Weak, + attack_step: i32, + attack_time: i32, + last_seen: i32, +} + +impl BlazeShootFireballGoal { + #[must_use] + pub const fn new(blaze: std::sync::Weak) -> Self { + Self { + blaze, + attack_step: 0, + attack_time: 0, + last_seen: 0, + } + } + + const fn get_follow_distance() -> f64 { + // TODO: use FOLLOW_RANGE + 48.0 + } +} + +impl Goal for BlazeShootFireballGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let Some(blaze) = self.blaze.upgrade() else { + return false; + }; + let target = blaze.entity.target.lock().await.clone(); + if target.is_some() { + // TODO: check is_alive + true + } else { + false + } + }) + } + + fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let Some(blaze) = self.blaze.upgrade() else { + return false; + }; + let target = blaze.entity.target.lock().await.clone(); + if target.is_some() { + // TODO: check is_alive + true + } else { + false + } + }) + } + + fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.attack_step = 0; + }) + } + + fn stop<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + if let Some(blaze) = self.blaze.upgrade() { + blaze.set_charged(false); + } + self.last_seen = 0; + }) + } + + fn should_run_every_tick(&self) -> bool { + true + } + + fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.attack_time -= 1; + + let Some(blaze) = self.blaze.upgrade() else { + return; + }; + + let target = blaze.entity.target.lock().await.clone(); + let Some(target) = target else { + return; + }; + + // TODO: hasLineOfSight check + let has_line_of_sight = true; + + if has_line_of_sight { + self.last_seen = 0; + } else { + self.last_seen += 1; + } + + let blaze_pos = blaze.entity.living_entity.entity.pos.load(); + let target_pos = target.get_entity().pos.load(); + + let dx = target_pos.x - blaze_pos.x; + let dy = target_pos.y - blaze_pos.y; + let dz = target_pos.z - blaze_pos.z; + + let distance_sq = dx * dx + dy * dy + dz * dz; + + if distance_sq < 4.0 { + if !has_line_of_sight { + return; + } + + if self.attack_time <= 0 { + self.attack_time = 20; + // TODO: doHurtTarget + } + + // TODO: set wanted position to target + } else if distance_sq < Self::get_follow_distance().powi(2) && has_line_of_sight { + let target_y_offset = target_pos.y + 0.5; // roughly target.getY(0.5) + let blaze_y_offset = blaze_pos.y + 0.5; // roughly blaze.getY(0.5) + let yd = target_y_offset - blaze_y_offset; + + if self.attack_time <= 0 { + self.attack_step += 1; + if self.attack_step == 1 { + self.attack_time = 60; + blaze.set_charged(true); + } else if self.attack_step <= 4 { + self.attack_time = 6; + } else { + self.attack_time = 100; + self.attack_step = 0; + blaze.set_charged(false); + } + + if self.attack_step > 1 { + let distance = distance_sq.sqrt(); + let sqd = distance.sqrt() * 0.5; + + // play shoot sound + blaze + .entity + .living_entity + .entity + .world + .load() + .broadcast_packet_all(&CWorldEvent::new( + 1018, + blaze.entity.living_entity.entity.block_pos.load(), + 0, + false, + )) + .await; + + for _ in 0..1 { + // Vanilla loops 1 time + // Calculate spread + let direction = { + let mut rng = rand::rng(); + let dir_x = (dx - 2.297 * sqd) + + rng.random_range(0.0..1.0) * (2.297 * sqd * 2.0); + let dir_z = (dz - 2.297 * sqd) + + rng.random_range(0.0..1.0) * (2.297 * sqd * 2.0); + Vector3::new(dir_x, yd, dir_z).normalize() + }; + + // Spawn SmallFireball + let world = blaze.entity.living_entity.entity.world.load(); + let uuid = uuid::Uuid::new_v4(); + + let mut pos = blaze.entity.living_entity.entity.pos.load(); + pos.y += blaze.entity.living_entity.entity.get_eye_height() - 0.1; + + let base_entity = Entity::from_uuid( + uuid, + world.clone(), + pos, + &pumpkin_data::entity::EntityType::SMALL_FIREBALL, + ); + + let fireball = SmallFireballEntity::new_shot( + base_entity, + &blaze.entity.living_entity.entity, + ); + fireball.thrown.entity.velocity.store(direction); + + world.spawn_entity(Arc::new(fireball)).await; + } + } + } + + // Look at target + blaze + .entity + .look_control + .lock() + .await + .look_at_entity(&*blaze, &target); + } else if self.last_seen < 5 { + // TODO: set wanted position to target + } + }) + } + + fn controls(&self) -> Controls { + Controls::MOVE | Controls::LOOK + } +} diff --git a/pumpkin/src/entity/ai/goal/mod.rs b/pumpkin/src/entity/ai/goal/mod.rs index 3f6b8dae5..1deeabbaf 100644 --- a/pumpkin/src/entity/ai/goal/mod.rs +++ b/pumpkin/src/entity/ai/goal/mod.rs @@ -5,6 +5,7 @@ pub mod active_target; pub mod ambient_stand; pub mod avoid_entity; pub mod beg; +pub mod blaze_attack; pub mod breed; pub mod chase_player; pub mod creeper_ignite; diff --git a/pumpkin/src/entity/mob/blaze.rs b/pumpkin/src/entity/mob/blaze.rs index a734379a2..ff720eab4 100644 --- a/pumpkin/src/entity/mob/blaze.rs +++ b/pumpkin/src/entity/mob/blaze.rs @@ -6,14 +6,13 @@ use crate::entity::{ Entity, NBTStorage, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, }; pub struct BlazeEntity { - entity: Arc, + pub entity: Arc, } impl BlazeEntity { @@ -30,8 +29,16 @@ impl BlazeEntity { let mut target_selector = mob_arc.entity.target_selector.lock().await; goal_selector.add_goal(0, Box::new(SwimGoal::default())); - // TODO: BlazeShootFireballGoal - goal_selector.add_goal(4, Box::new(MeleeAttackGoal::new(1.0, false))); + + goal_selector.add_goal( + 4, + Box::new( + crate::entity::ai::goal::blaze_attack::BlazeShootFireballGoal::new( + Arc::downgrade(&mob_arc), + ), + ), + ); + goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( 8, @@ -47,6 +54,26 @@ impl BlazeEntity { mob_arc } + + pub const fn set_charged(&self, _charged: bool) { + // TODO: + // let flags = &self.entity.living_entity.entity.flags; + + // let new_je_flags = if charged { + // flags.fetch_or(1, Ordering::Relaxed) | 1 + // } else { + // flags.fetch_and(!1, Ordering::Relaxed) & !1 + // }; + // self.entity + // .living_entity + // .entity + // .send_meta_data(&[Metadata::new( + // TrackedData::FLAGS_ID, + // MetaDataType::BYTE, + // new_je_flags, + // )]) + // .await; + } } impl NBTStorage for BlazeEntity {} diff --git a/pumpkin/src/entity/projectile/fireball.rs b/pumpkin/src/entity/projectile/fireball.rs new file mode 100644 index 000000000..b298b3b21 --- /dev/null +++ b/pumpkin/src/entity/projectile/fireball.rs @@ -0,0 +1,98 @@ +use std::sync::Arc; +use std::sync::atomic::AtomicBool; + +use crate::{ + entity::{ + Entity, EntityBase, EntityBaseFuture, NBTStorage, + projectile::{ProjectileHit, ThrownItemEntity}, + }, + server::Server, +}; + +const EXPLOSION_POWER: f32 = 1.0; + +pub struct FireballEntity { + pub thrown: ThrownItemEntity, + pub explosion_power: f32, +} + +impl FireballEntity { + #[must_use] + pub const fn new(entity: Entity) -> Self { + let thrown = ThrownItemEntity { + entity, + owner_id: None, + collides_with_projectiles: false, + has_hit: AtomicBool::new(false), + }; + + Self { + thrown, + explosion_power: EXPLOSION_POWER, + } + } + + #[must_use] + pub fn new_shot(entity: Entity, shooter: &Entity) -> Self { + let thrown = ThrownItemEntity::new(entity, shooter); + Self { + thrown, + explosion_power: EXPLOSION_POWER, + } + } +} + +impl NBTStorage for FireballEntity {} + +impl EntityBase for FireballEntity { + fn tick<'a>( + &'a self, + caller: &'a Arc, + server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { self.thrown.process_tick(caller, server).await }) + } + + fn get_entity(&self) -> &Entity { + self.thrown.get_entity() + } + + fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } + + fn on_hit(&self, hit: ProjectileHit) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let world = self.get_entity().world.load(); + + // Handle entity/block hit + if let ProjectileHit::Entity { ref entity, .. } = hit { + let entity_clone = entity.clone(); + + tokio::spawn(async move { + // Fireball does 6.0 damage in vanilla + entity_clone.get_entity().set_on_fire_for(5.0); + let _ = entity_clone + .damage( + entity_clone.as_ref(), + 6.0, + pumpkin_data::damage::DamageType::FIREBALL, + ) + .await; + }); + } + + let hit_pos = hit.hit_pos(); + // Explosion sets fire if mob griefing is enabled (assuming true for now) + world.explode(hit_pos, self.explosion_power).await; + }) + } +} diff --git a/pumpkin/src/entity/projectile/mod.rs b/pumpkin/src/entity/projectile/mod.rs index ce754a0c6..66b2b61de 100644 --- a/pumpkin/src/entity/projectile/mod.rs +++ b/pumpkin/src/entity/projectile/mod.rs @@ -13,9 +13,11 @@ pub mod arrow; pub mod egg; pub mod ender_pearl; pub mod eye_of_ender; +pub mod fireball; pub mod firework_rocket; pub mod lingering_potion; pub mod shulker_bullet; +pub mod small_fireball; pub mod snowball; pub mod splash_potion; pub mod wind_charge; @@ -31,6 +33,8 @@ pub fn is_projectile(entity_type: &EntityType) -> bool { || *entity_type == EntityType::LINGERING_POTION || *entity_type == EntityType::ENDER_PEARL || *entity_type == EntityType::SHULKER_BULLET + || *entity_type == EntityType::FIREBALL + || *entity_type == EntityType::SMALL_FIREBALL } pub struct ThrownItemEntity { diff --git a/pumpkin/src/entity/projectile/small_fireball.rs b/pumpkin/src/entity/projectile/small_fireball.rs new file mode 100644 index 000000000..98b9f8abd --- /dev/null +++ b/pumpkin/src/entity/projectile/small_fireball.rs @@ -0,0 +1,103 @@ +use std::sync::Arc; +use std::sync::atomic::AtomicBool; + +use crate::{ + entity::{ + Entity, EntityBase, EntityBaseFuture, NBTStorage, + projectile::{ProjectileHit, ThrownItemEntity}, + }, + server::Server, +}; + +pub struct SmallFireballEntity { + pub thrown: ThrownItemEntity, +} + +impl SmallFireballEntity { + #[must_use] + pub const fn new(entity: Entity) -> Self { + let thrown = ThrownItemEntity { + entity, + owner_id: None, + collides_with_projectiles: false, + has_hit: AtomicBool::new(false), + }; + + Self { thrown } + } + + #[must_use] + pub fn new_shot(entity: Entity, shooter: &Entity) -> Self { + let thrown = ThrownItemEntity::new(entity, shooter); + Self { thrown } + } +} + +impl NBTStorage for SmallFireballEntity {} + +impl EntityBase for SmallFireballEntity { + fn tick<'a>( + &'a self, + caller: &'a Arc, + server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { self.thrown.process_tick(caller, server).await }) + } + + fn get_entity(&self) -> &Entity { + self.thrown.get_entity() + } + + fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } + + fn on_hit(&self, hit: ProjectileHit) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + match hit { + ProjectileHit::Entity { ref entity, .. } => { + let entity_clone = entity.clone(); + + tokio::spawn(async move { + entity_clone.get_entity().set_on_fire_for(5.0); + let _ = entity_clone + .damage( + entity_clone.as_ref(), + 5.0, + pumpkin_data::damage::DamageType::FIREBALL, + ) + .await; + }); + } + ProjectileHit::Block { pos, face, .. } => { + // Try to place fire + let block_to_place = match face { + pumpkin_data::BlockDirection::Up => pos.up(), + pumpkin_data::BlockDirection::Down => pos.down(), + pumpkin_data::BlockDirection::North => pos.north(), + pumpkin_data::BlockDirection::South => pos.south(), + pumpkin_data::BlockDirection::West => pos.west(), + pumpkin_data::BlockDirection::East => pos.east(), + }; + let world = self.get_entity().world.load(); + let fire_state = pumpkin_data::Block::FIRE.default_state.id; + world + .set_block_state( + &block_to_place, + fire_state, + pumpkin_world::world::BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + }) + } +} diff --git a/pumpkin/src/entity/type.rs b/pumpkin/src/entity/type.rs index c39ae7834..459c57cba 100644 --- a/pumpkin/src/entity/type.rs +++ b/pumpkin/src/entity/type.rs @@ -97,9 +97,11 @@ use crate::entity::projectile::arrow::ArrowEntity; use crate::entity::projectile::egg::EggEntity; use crate::entity::projectile::ender_pearl::EnderPearlEntity; use crate::entity::projectile::eye_of_ender::EyeOfEnder; +use crate::entity::projectile::fireball::FireballEntity; use crate::entity::projectile::firework_rocket::FireworkRocketEntity; use crate::entity::projectile::lingering_potion::LingeringPotionEntity; use crate::entity::projectile::shulker_bullet::ShulkerBulletEntity; +use crate::entity::projectile::small_fireball::SmallFireballEntity; use crate::entity::projectile::snowball::SnowballEntity; use crate::entity::projectile::splash_potion::SplashPotionEntity; use crate::entity::projectile::wind_charge::WindChargeEntity; @@ -239,6 +241,8 @@ pub async fn from_type( } id if id == EntityType::ARROW.id => Arc::new(ArrowEntity::new(entity, None)), id if id == EntityType::SPECTRAL_ARROW.id => Arc::new(ArrowEntity::new(entity, None)), + id if id == EntityType::FIREBALL.id => Arc::new(FireballEntity::new(entity)), + id if id == EntityType::SMALL_FIREBALL.id => Arc::new(SmallFireballEntity::new(entity)), id if id == EntityType::WIND_CHARGE.id => { let thrown = ThrownItemEntity { entity,