feat: added some Love 💞

This commit is contained in:
Alexander Medvedev
2026-07-25 12:08:59 +02:00
parent cba7578e28
commit 25ba4c6e40
10 changed files with 369 additions and 152 deletions

View File

@@ -1,7 +1,6 @@
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::{
data_component::DataComponent,
data_component_impl::DataComponentImpl,
item::Item,
tag::{RegistryKey, get_tag_ids},
};
@@ -120,27 +119,15 @@ impl<'a> FindArg<'a> for ItemArgumentConsumer {
)
{
// Match the DataComponent key
if let Some(data_comp) = DataComponent::try_from_name(comp_key)
{
// Handle Profile or other data components
match data_comp {
DataComponent::Profile => {
if let Some(profile_impl) = pumpkin_data::data_component_impl::ProfileImpl::read_data(&nbt_tag) {
patch.push((data_comp, Some(profile_impl.to_dyn())));
}
}
DataComponent::CustomData => {
if let pumpkin_nbt::tag::NbtTag::Compound(compound) = nbt_tag {
patch.push((data_comp, Some(pumpkin_data::data_component_impl::CustomDataImpl { data: compound }.to_dyn())));
}
}
DataComponent::CustomName => {
if let pumpkin_nbt::tag::NbtTag::String(text_str) = nbt_tag {
patch.push((data_comp, Some(pumpkin_data::data_component_impl::CustomNameImpl { name: pumpkin_util::text::TextComponent::text(String::from(text_str)) }.to_dyn())));
}
}
_ => {}
}
if let (Some(data_comp), Some(comp_impl)) = (
DataComponent::try_from_name(comp_key),
pumpkin_data::data_component_impl::read_data(
DataComponent::try_from_name(comp_key)
.unwrap_or(DataComponent::CustomData),
&nbt_tag,
),
) {
patch.push((data_comp, Some(comp_impl)));
}
}
}

View File

@@ -0,0 +1,141 @@
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_protocol::java::client::play::Metadata;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering::Relaxed};
use crate::entity::mob::Mob;
pub const BABY_START_AGE: i32 = -24000;
pub const FORCED_AGE_PARTICLE_TICKS: i32 = 40;
pub struct AgeableData {
pub forced_age: AtomicI32,
pub forced_age_timer: AtomicI32,
pub age_locked: AtomicBool,
pub age_lock_particle_timer: AtomicI32,
}
impl Default for AgeableData {
fn default() -> Self {
Self {
forced_age: AtomicI32::new(0),
forced_age_timer: AtomicI32::new(0),
age_locked: AtomicBool::new(false),
age_lock_particle_timer: AtomicI32::new(0),
}
}
}
pub trait AgeableMob: Mob {
fn get_ageable_data(&self) -> &AgeableData;
fn get_baby_start_age(&self) -> i32 {
BABY_START_AGE
}
fn is_baby(&self) -> bool {
self.get_mob_entity().living_entity.entity.age.load(Relaxed) < 0
}
fn set_baby(&self, baby: bool) {
self.set_age(if baby { self.get_baby_start_age() } else { 0 });
}
fn get_age(&self) -> i32 {
self.get_mob_entity().living_entity.entity.age.load(Relaxed)
}
fn set_age(&self, new_age: i32) {
let mob = self.get_mob_entity();
let entity = &mob.living_entity.entity;
let old_age = entity.age.swap(new_age, Relaxed);
if (old_age < 0 && new_age >= 0) || (old_age >= 0 && new_age < 0) {
let is_baby = new_age < 0;
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
is_baby,
)],
None,
);
}
}
fn is_age_locked(&self) -> bool {
self.get_ageable_data().age_locked.load(Relaxed)
}
fn set_age_locked(&self, locked: bool) {
self.get_ageable_data().age_locked.store(locked, Relaxed);
}
fn can_age_up(&self) -> bool {
self.is_baby() && !self.is_age_locked()
}
fn age_up(&self, seconds: i32, forced: bool) {
let mut age = self.get_age();
let old_age = age;
age += seconds * 20;
if age > 0 {
age = 0;
}
let delta = age - old_age;
self.set_age(age);
let data = self.get_ageable_data();
if forced {
data.forced_age.fetch_add(delta, Relaxed);
if data.forced_age_timer.load(Relaxed) == 0 {
data.forced_age_timer.store(40, Relaxed);
}
}
if self.get_age() == 0 {
self.set_age(data.forced_age.load(Relaxed));
}
}
#[must_use]
fn get_speed_up_seconds_when_feeding(ticks_until_adult: i32) -> i32 {
(ticks_until_adult as f32 / 20.0 * 0.1) as i32
}
fn write_ageable_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) {
if self.can_be_a_baby() {
nbt.put_int("Age", self.get_age());
nbt.put_int(
"ForcedAge",
self.get_ageable_data().forced_age.load(Relaxed),
);
nbt.put_bool("AgeLocked", self.is_age_locked());
}
}
fn read_ageable_nbt(&self, nbt: &pumpkin_nbt::compound::NbtCompound) {
if self.can_be_a_baby() {
self.set_age(nbt.get_int("Age").unwrap_or(0));
self.get_ageable_data()
.forced_age
.store(nbt.get_int("ForcedAge").unwrap_or(0), Relaxed);
self.set_age_locked(nbt.get_bool("AgeLocked").unwrap_or(false));
}
}
fn can_be_a_baby(&self) -> bool {
true
}
fn ageable_ai_step(&self) {
if self.can_age_up() {
let age = self.get_age() + 1;
self.set_age(age);
} else if self.get_age() > 0 {
let age = self.get_age() - 1;
self.set_age(age);
}
}
}

View File

@@ -601,7 +601,19 @@ impl<T: Mob + Send + 'static> EntityBase for T {
}
if mob_entity.love_ticks.load(Relaxed) > 0 {
mob_entity.love_ticks.fetch_sub(1, Relaxed);
let ticks = mob_entity.love_ticks.fetch_sub(1, Relaxed);
if ticks % 10 == 0 {
let entity = &mob_entity.living_entity.entity;
let pos = entity.pos.load();
let world = entity.world.load();
world.spawn_particle(
pos + Vector3::new(0.0, f64::from(entity.height()) + 0.5, 0.0),
Vector3::new(0.5, 0.5, 0.5),
1.0,
1,
pumpkin_data::particle::Particle::Heart,
);
}
}
self.mob_tick(caller).await;

View File

@@ -80,6 +80,7 @@ use std::sync::{
use tokio::sync::Mutex;
use uuid::Uuid;
pub mod ageable;
pub mod ai;
pub mod area_effect_cloud;
pub mod attributes;

View File

@@ -0,0 +1,105 @@
use std::sync::Arc;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use crate::entity::{EntityBaseFuture, mob::Mob, player::Player};
use pumpkin_util::math::vector3::Vector3;
pub trait Animal: Mob {
fn is_food(&self, item_stack: &ItemStack) -> bool;
fn play_eating_sound(&self, sound: Sound) {
let mob_entity = self.get_mob_entity();
let entity = &mob_entity.living_entity.entity;
let world = entity.world.load();
world.play_sound(sound, SoundCategory::Neutral, &entity.pos.load());
}
fn write_animal_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) {
let mob_entity = self.get_mob_entity();
let in_love = mob_entity
.love_ticks
.load(std::sync::atomic::Ordering::Relaxed);
nbt.put_int("InLove", in_love);
if let Some(uuid) = mob_entity.breeder.load() {
nbt.put_uuid("LoveCause", uuid);
}
}
fn read_animal_nbt(&self, nbt: &pumpkin_nbt::compound::NbtCompound) {
let mob_entity = self.get_mob_entity();
let in_love = nbt.get_int("InLove").unwrap_or(0);
let love_cause = nbt.get_uuid("LoveCause");
mob_entity.set_love_ticks(in_love, love_cause);
}
fn animal_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
ambient_sound: Sound,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let mob_entity = self.get_mob_entity();
if self.is_food(item_stack) {
let age = mob_entity
.living_entity
.entity
.age
.load(std::sync::atomic::Ordering::Relaxed);
if age >= 0 && mob_entity.is_breeding_ready() && !mob_entity.is_in_love() {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
mob_entity.set_love_ticks(600, Some(player.gameprofile.id));
let entity = &mob_entity.living_entity.entity;
let world = entity.world.load();
let pos = entity.pos.load();
world.send_entity_status(
entity,
pumpkin_data::entity::EntityStatus::InLoveHearts,
);
world.spawn_particle(
pos + Vector3::new(0.0, f64::from(entity.height()), 0.0),
Vector3::new(0.5, 0.5, 0.5),
1.0,
7,
Particle::Heart,
);
world.play_sound(ambient_sound, SoundCategory::Neutral, &entity.pos.load());
return true;
}
if age < 0 {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
let speedup = (-age / 10).max(1);
mob_entity
.living_entity
.entity
.age
.fetch_add(speedup, std::sync::atomic::Ordering::Relaxed);
let entity = &mob_entity.living_entity.entity;
let world = entity.world.load();
let pos = entity.pos.load();
world.spawn_particle(
pos + Vector3::new(0.0, f64::from(entity.height()), 0.0),
Vector3::new(0.5, 0.5, 0.5),
1.0,
7,
Particle::HappyVillager,
);
self.play_eating_sound(ambient_sound);
return true;
}
}
mob_entity.mob_interact(player, item_stack).await
})
}
}

View File

@@ -5,22 +5,22 @@ use std::sync::{
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::sound::Sound;
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_data::{entity::EntityType, item::Item};
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_util::math::vector3::Vector3;
use rand::RngExt;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::AgeableMob,
ai::goal::{
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
tempt::TemptGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
use pumpkin_nbt::compound::NbtCompound;
@@ -41,6 +41,7 @@ pub struct ChickenEntity {
pub mob_entity: MobEntity,
pub variant: AtomicU8,
egg_lay_time: AtomicI32,
pub ageable_data: crate::entity::ageable::AgeableData,
}
impl ChickenEntity {
@@ -51,6 +52,7 @@ impl ChickenEntity {
mob_entity,
variant: AtomicU8::new(1), // Default to temperate
egg_lay_time: AtomicI32::new(egg_lay_time),
ageable_data: crate::entity::ageable::AgeableData::default(),
};
let mob_arc = Arc::new(chicken);
let mob_weak: Weak<dyn Mob> = {
@@ -78,10 +80,18 @@ impl ChickenEntity {
}
}
impl crate::entity::ageable::AgeableMob for ChickenEntity {
fn get_ageable_data(&self) -> &crate::entity::ageable::AgeableData {
&self.ageable_data
}
}
impl NBTStorage for ChickenEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
nbt.put_int("EggLayTime", self.egg_lay_time.load(Ordering::Relaxed));
let variant_str = match self.variant.load(Ordering::Relaxed) {
0 => "minecraft:cold",
@@ -95,6 +105,8 @@ impl NBTStorage for ChickenEntity {
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
self.egg_lay_time
.store(nbt.get_int("EggLayTime").unwrap_or(6000), Ordering::Relaxed);
if let Some(variant_str) = nbt.get_string("variant") {
@@ -112,6 +124,12 @@ impl NBTStorage for ChickenEntity {
}
}
impl super::animal::Animal for ChickenEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id)
}
}
impl Mob for ChickenEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
@@ -172,32 +190,7 @@ impl Mob for ChickenEntity {
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let is_food = TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id);
if is_food && self.is_breeding_ready() && !self.is_in_love() {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.mob_entity
.set_love_ticks(600, Some(player.gameprofile.id));
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
let pos = entity.pos.load();
world.spawn_particle(
pos + Vector3::new(0.0, f64::from(entity.height()), 0.0),
Vector3::new(0.5, 0.5, 0.5),
1.0,
7,
Particle::Heart,
);
world.play_sound(
Sound::EntityChickenAmbient,
SoundCategory::Neutral,
&entity.pos.load(),
);
return true;
}
self.mob_entity.mob_interact(player, item_stack).await
})
use super::animal::Animal;
self.animal_interact(player, item_stack, Sound::EntityChickenAmbient)
}
}

View File

@@ -1,19 +1,19 @@
use std::sync::{Arc, Weak};
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::sound::Sound;
use pumpkin_data::{entity::EntityType, item::Item};
use pumpkin_util::math::vector3::Vector3;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
Entity, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::AgeableMob,
ai::goal::{
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
tempt::TemptGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
use pumpkin_nbt::compound::NbtCompound;
@@ -25,12 +25,16 @@ const TEMPT_ITEMS: &[&Item] = &[&Item::WHEAT];
/// Wiki: <https://minecraft.wiki/w/Cow>
pub struct CowEntity {
pub mob_entity: MobEntity,
pub ageable_data: crate::entity::ageable::AgeableData,
}
impl CowEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let cow = Self { mob_entity };
let cow = Self {
mob_entity,
ageable_data: crate::entity::ageable::AgeableData::default(),
};
let mob_arc = Arc::new(cow);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@@ -57,13 +61,33 @@ impl CowEntity {
}
}
impl crate::entity::ageable::AgeableMob for CowEntity {
fn get_ageable_data(&self) -> &crate::entity::ageable::AgeableData {
&self.ageable_data
}
}
impl NBTStorage for CowEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
self.mob_entity.living_entity.write_nbt(nbt)
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
self.mob_entity.living_entity.read_nbt_non_mut(nbt)
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
})
}
}
impl super::animal::Animal for CowEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id)
}
}
@@ -77,32 +101,7 @@ impl Mob for CowEntity {
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let is_food = TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id);
if is_food && self.is_breeding_ready() && !self.is_in_love() {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.mob_entity
.set_love_ticks(600, Some(player.gameprofile.id));
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
let pos = entity.pos.load();
world.spawn_particle(
pos + Vector3::new(0.0, f64::from(entity.height()), 0.0),
Vector3::new(0.5, 0.5, 0.5),
1.0,
7,
Particle::Heart,
);
world.play_sound(
Sound::EntityCowAmbient,
SoundCategory::Neutral,
&entity.pos.load(),
);
return true;
}
self.mob_entity.mob_interact(player, item_stack).await
})
use super::animal::Animal;
self.animal_interact(player, item_stack, Sound::EntityCowAmbient)
}
}

View File

@@ -1,4 +1,5 @@
pub mod allay;
pub mod animal;
pub mod armadillo;
pub mod axolotl;
pub mod bee;

View File

@@ -1,19 +1,19 @@
use std::sync::{Arc, Weak};
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::sound::Sound;
use pumpkin_data::{entity::EntityType, item::Item};
use pumpkin_util::math::vector3::Vector3;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
Entity, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::AgeableMob,
ai::goal::{
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
tempt::TemptGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
use pumpkin_nbt::compound::NbtCompound;
@@ -30,12 +30,16 @@ const PIG_FOOD: &[&Item] = &[
/// Wiki: <https://minecraft.wiki/w/Pig>
pub struct PigEntity {
pub mob_entity: MobEntity,
pub ageable_data: crate::entity::ageable::AgeableData,
}
impl PigEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let pig = Self { mob_entity };
let pig = Self {
mob_entity,
ageable_data: crate::entity::ageable::AgeableData::default(),
};
let mob_arc = Arc::new(pig);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@@ -62,13 +66,33 @@ impl PigEntity {
}
}
impl crate::entity::ageable::AgeableMob for PigEntity {
fn get_ageable_data(&self) -> &crate::entity::ageable::AgeableData {
&self.ageable_data
}
}
impl NBTStorage for PigEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
self.mob_entity.living_entity.write_nbt(nbt)
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
self.mob_entity.living_entity.read_nbt_non_mut(nbt)
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
})
}
}
impl super::animal::Animal for PigEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
PIG_FOOD.iter().any(|i| i.id == item_stack.item.id)
}
}
@@ -82,32 +106,7 @@ impl Mob for PigEntity {
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let is_food = PIG_FOOD.iter().any(|i| i.id == item_stack.item.id);
if is_food && self.is_breeding_ready() && !self.is_in_love() {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.mob_entity
.set_love_ticks(600, Some(player.gameprofile.id));
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
let pos = entity.pos.load();
world.spawn_particle(
pos + Vector3::new(0.0, f64::from(entity.height()), 0.0),
Vector3::new(0.5, 0.5, 0.5),
1.0,
7,
Particle::Heart,
);
world.play_sound(
Sound::EntityPigAmbient,
SoundCategory::Neutral,
&entity.pos.load(),
);
return true;
}
self.mob_entity.mob_interact(player, item_stack).await
})
use super::animal::Animal;
self.animal_interact(player, item_stack, Sound::EntityPigAmbient)
}
}

View File

@@ -10,7 +10,7 @@ use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::java::client::play::Metadata;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
Entity, EntityBaseFuture, NBTStorage, NbtFuture,
ai::goal::{
breed::BreedGoal, eat_grass::EatGrassGoal, escape_danger::EscapeDangerGoal,
follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal,
@@ -22,9 +22,7 @@ use crate::entity::{
};
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_util::math::vector3::Vector3;
use pumpkin_data::sound::Sound;
const TEMPT_ITEMS: &[&Item] = &[&Item::WHEAT];
@@ -129,6 +127,12 @@ impl NBTStorage for SheepEntity {
}
}
impl super::animal::Animal for SheepEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id)
}
}
impl Mob for SheepEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
@@ -145,32 +149,7 @@ impl Mob for SheepEntity {
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let is_food = TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id);
if is_food && self.is_breeding_ready() && !self.is_in_love() {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.mob_entity
.set_love_ticks(600, Some(player.gameprofile.id));
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
let pos = entity.pos.load();
world.spawn_particle(
pos + Vector3::new(0.0, f64::from(entity.height()), 0.0),
Vector3::new(0.5, 0.5, 0.5),
1.0,
7,
Particle::Heart,
);
world.play_sound(
Sound::EntitySheepAmbient,
SoundCategory::Neutral,
&entity.pos.load(),
);
return true;
}
self.mob_entity.mob_interact(player, item_stack).await
})
use super::animal::Animal;
self.animal_interact(player, item_stack, Sound::EntitySheepAmbient)
}
}