feat: villager Professions
This commit is contained in:
parent
daa0111009
commit
5f60d23804
@ -28,6 +28,16 @@ pub fn build() -> TokenStream {
|
||||
let i = n.as_i64().unwrap();
|
||||
(quote! { Int }, quote! { i64 }, quote! { #i })
|
||||
}
|
||||
Value::Object(obj) => {
|
||||
let default_val = obj.get("default").expect("Object game rule missing default value");
|
||||
match default_val {
|
||||
Value::Number(n) if n.is_i64() => {
|
||||
let i = n.as_i64().unwrap();
|
||||
(quote! { Int }, quote! { i64 }, quote! { #i })
|
||||
}
|
||||
_ => panic!("Unsupported default value type in object for key '{raw_name}'"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Unsupported value type for key '{raw_name}'"),
|
||||
};
|
||||
|
||||
|
||||
@ -3,10 +3,11 @@
|
||||
use crate::attributes::Attributes;
|
||||
use crate::data_component::DataComponent;
|
||||
use crate::data_component::DataComponent::{
|
||||
AttributeModifiers, BlocksAttacks, ChargedProjectiles, Consumable, CustomData, CustomName,
|
||||
Damage, DamageResistant, DeathProtection, Enchantable, Enchantments, Equippable,
|
||||
FireworkExplosion, Fireworks, Food, ItemModel, ItemName, JukeboxPlayable, MapId, MaxDamage,
|
||||
MaxStackSize, PotionContents, StoredEnchantments, Tool, Unbreakable, UseCooldown, Weapon,
|
||||
AttributeModifiers, BlockEntityData, BlocksAttacks, BundleContents, ChargedProjectiles,
|
||||
Consumable, Container, CustomData, CustomName, Damage, DamageResistant, DeathProtection,
|
||||
Enchantable, Enchantments, Equippable, FireworkExplosion, Fireworks, Food, ItemModel, ItemName,
|
||||
JukeboxPlayable, MapId, MaxDamage, MaxStackSize, PotionContents, StoredEnchantments, Tool,
|
||||
Unbreakable, UseCooldown, Weapon,
|
||||
};
|
||||
use crate::effect::{self, StatusEffect};
|
||||
use crate::entity_type::EntityType;
|
||||
@ -70,6 +71,15 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option<Box<dyn DataCompone
|
||||
DataComponent::ChargedProjectiles => {
|
||||
Some(ChargedProjectilesImpl::read_data(data)?.to_dyn())
|
||||
}
|
||||
DataComponent::BlockEntityData => Some(BlockEntityDataImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::BundleContents => Some(BundleContentsImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::Container => Some(ContainerImpl::read_data(data)?.to_dyn()),
|
||||
DataComponent::WrittenBookContent => {
|
||||
Some(WrittenBookContentImpl::read_data(data)?.to_dyn())
|
||||
}
|
||||
DataComponent::WritableBookContent => {
|
||||
Some(WritableBookContentImpl::read_data(data)?.to_dyn())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -1559,8 +1569,79 @@ impl DataComponentImpl for ChargedProjectilesImpl {
|
||||
default_impl!(ChargedProjectiles);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct BundleContentsImpl;
|
||||
#[derive(Clone)]
|
||||
pub struct BundleContentsImpl {
|
||||
pub items: Vec<crate::item_stack::ItemStack>,
|
||||
}
|
||||
impl PartialEq for BundleContentsImpl {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
impl Eq for BundleContentsImpl {}
|
||||
impl std::fmt::Debug for BundleContentsImpl {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "BundleContentsImpl")
|
||||
}
|
||||
}
|
||||
impl BundleContentsImpl {
|
||||
pub fn read_data(tag: &NbtTag) -> Option<Self> {
|
||||
let mut items = Vec::new();
|
||||
if let NbtTag::List(l) = tag {
|
||||
for item_tag in l {
|
||||
if let NbtTag::Compound(c) = item_tag
|
||||
&& let Some(stack) = crate::item_stack::ItemStack::read_item_stack(c)
|
||||
{
|
||||
items.push(stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Self { items })
|
||||
}
|
||||
|
||||
pub fn get_weight(&self) -> u32 {
|
||||
self.items
|
||||
.iter()
|
||||
.map(|item| item.item_count as u32 * (64 / item.get_max_stack_size() as u32).max(1))
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn try_insert(&mut self, stack: &mut crate::item_stack::ItemStack) -> bool {
|
||||
if stack.is_empty() || stack.get_data_component::<BundleContentsImpl>().is_some() {
|
||||
return false; // Can't put bundles in bundles
|
||||
}
|
||||
let weight_per_item = (64 / stack.get_max_stack_size() as u32).max(1);
|
||||
let mut inserted_anything = false;
|
||||
|
||||
while stack.item_count > 0 && self.get_weight() + weight_per_item <= 64 {
|
||||
if let Some(top) = self.items.first_mut()
|
||||
&& crate::item_stack::ItemStack::are_items_and_components_equal(top, stack)
|
||||
&& top.item_count < top.get_max_stack_size()
|
||||
{
|
||||
top.item_count += 1;
|
||||
stack.item_count -= 1;
|
||||
inserted_anything = true;
|
||||
continue;
|
||||
}
|
||||
self.items.insert(0, stack.copy_with_count(1));
|
||||
stack.item_count -= 1;
|
||||
inserted_anything = true;
|
||||
}
|
||||
|
||||
inserted_anything
|
||||
}
|
||||
|
||||
pub fn try_extract(&mut self) -> Option<crate::item_stack::ItemStack> {
|
||||
if self.items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.items.remove(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl DataComponentImpl for BundleContentsImpl {
|
||||
default_impl!(BundleContents);
|
||||
}
|
||||
/// Status effect instance for potion contents
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct StatusEffectInstance {
|
||||
@ -1898,9 +1979,53 @@ pub struct PotionDurationScaleImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct SuspiciousStewEffectsImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct WritableBookContentImpl;
|
||||
pub struct WritableBookContentImpl {
|
||||
pub pages: Vec<String>,
|
||||
}
|
||||
|
||||
impl WritableBookContentImpl {
|
||||
pub fn read_data(tag: &NbtTag) -> Option<Self> {
|
||||
let mut pages = Vec::new();
|
||||
if let NbtTag::Compound(c) = tag
|
||||
&& let Some(NbtTag::List(l)) = c.get("pages")
|
||||
{
|
||||
for _ in l {
|
||||
pages.push(String::new());
|
||||
}
|
||||
}
|
||||
Some(Self { pages })
|
||||
}
|
||||
}
|
||||
|
||||
use crate::data_component::DataComponent::{WritableBookContent, WrittenBookContent};
|
||||
|
||||
impl DataComponentImpl for WritableBookContentImpl {
|
||||
default_impl!(WritableBookContent);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct WrittenBookContentImpl;
|
||||
pub struct WrittenBookContentImpl {
|
||||
pub pages: Vec<String>,
|
||||
}
|
||||
|
||||
impl WrittenBookContentImpl {
|
||||
pub fn read_data(tag: &NbtTag) -> Option<Self> {
|
||||
let mut pages = Vec::new();
|
||||
if let NbtTag::Compound(c) = tag
|
||||
&& let Some(NbtTag::List(l)) = c.get("pages")
|
||||
{
|
||||
for _ in l {
|
||||
pages.push(String::new());
|
||||
}
|
||||
}
|
||||
Some(Self { pages })
|
||||
}
|
||||
}
|
||||
|
||||
impl DataComponentImpl for WrittenBookContentImpl {
|
||||
default_impl!(WrittenBookContent);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct TrimImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
@ -1909,8 +2034,22 @@ pub struct DebugStickStateImpl;
|
||||
pub struct EntityDataImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct BucketEntityDataImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct BlockEntityDataImpl;
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BlockEntityDataImpl {
|
||||
pub nbt: pumpkin_nbt::compound::NbtCompound,
|
||||
}
|
||||
impl BlockEntityDataImpl {
|
||||
pub fn read_data(tag: &NbtTag) -> Option<Self> {
|
||||
if let NbtTag::Compound(c) = tag {
|
||||
Some(Self { nbt: c.clone() })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
impl DataComponentImpl for BlockEntityDataImpl {
|
||||
default_impl!(BlockEntityData);
|
||||
}
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct InstrumentImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
@ -2129,8 +2268,42 @@ pub struct BannerPatternsImpl;
|
||||
pub struct BaseColorImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct PotDecorationsImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct ContainerImpl;
|
||||
#[derive(Clone)]
|
||||
pub struct ContainerImpl {
|
||||
pub items: Vec<(u8, crate::item_stack::ItemStack)>,
|
||||
}
|
||||
impl PartialEq for ContainerImpl {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
impl Eq for ContainerImpl {}
|
||||
impl std::fmt::Debug for ContainerImpl {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "ContainerImpl")
|
||||
}
|
||||
}
|
||||
impl ContainerImpl {
|
||||
pub fn read_data(tag: &NbtTag) -> Option<Self> {
|
||||
let mut items = Vec::new();
|
||||
if let NbtTag::List(l) = tag {
|
||||
for item_tag in l {
|
||||
if let NbtTag::Compound(c) = item_tag
|
||||
&& let Some(slot) = c.get_int("slot")
|
||||
&& let Some(item_compound) = c.get_compound("item")
|
||||
&& let Some(stack) =
|
||||
crate::item_stack::ItemStack::read_item_stack(item_compound)
|
||||
{
|
||||
items.push((slot as u8, stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Self { items })
|
||||
}
|
||||
}
|
||||
impl DataComponentImpl for ContainerImpl {
|
||||
default_impl!(Container);
|
||||
}
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct BlockStateImpl;
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
|
||||
@ -18,6 +18,7 @@ pub struct MerchantScreenHandler {
|
||||
behaviour: ScreenHandlerBehaviour,
|
||||
selected_offer: usize,
|
||||
pub offers: Vec<pumpkin_protocol::java::client::play::MerchantOffer>,
|
||||
pub on_trade: Option<Box<dyn Fn(usize) + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl MerchantScreenHandler {
|
||||
@ -32,6 +33,7 @@ impl MerchantScreenHandler {
|
||||
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Merchant)),
|
||||
selected_offer: 0,
|
||||
offers,
|
||||
on_trade: None,
|
||||
};
|
||||
|
||||
inventory.on_open().await;
|
||||
@ -183,21 +185,26 @@ impl ScreenHandler for MerchantScreenHandler {
|
||||
let result_stack = result_slot.get_cloned_stack().await;
|
||||
if !result_stack.is_empty() {
|
||||
// Consume inputs
|
||||
let offer = &self.offers[self.selected_offer];
|
||||
let (count_a, count_b, offer_xp) = {
|
||||
let offer = &mut self.offers[self.selected_offer];
|
||||
offer.uses += 1;
|
||||
let count_b = offer.cost_b.as_ref().map(|c| c.0.item_count);
|
||||
(offer.base_cost_a.0.item_count, count_b, offer.xp)
|
||||
};
|
||||
|
||||
let input_a = self.inventory.get_stack(0).await;
|
||||
let mut input_a = input_a.lock().await;
|
||||
input_a.decrement(offer.base_cost_a.0.item_count);
|
||||
input_a.decrement(count_a);
|
||||
if input_a.is_empty() {
|
||||
*input_a = ItemStack::EMPTY.clone();
|
||||
}
|
||||
drop(input_a);
|
||||
self.get_behaviour().slots[0].mark_dirty().await;
|
||||
|
||||
if let Some(cost_b) = &offer.cost_b {
|
||||
if let Some(count_b) = count_b {
|
||||
let input_b = self.inventory.get_stack(1).await;
|
||||
let mut input_b = input_b.lock().await;
|
||||
input_b.decrement(cost_b.0.item_count);
|
||||
input_b.decrement(count_b);
|
||||
if input_b.is_empty() {
|
||||
*input_b = ItemStack::EMPTY.clone();
|
||||
}
|
||||
@ -206,7 +213,11 @@ impl ScreenHandler for MerchantScreenHandler {
|
||||
}
|
||||
|
||||
// Award XP
|
||||
player.award_experience(offer.xp).await;
|
||||
player.award_experience(offer_xp).await;
|
||||
|
||||
if let Some(on_trade) = &self.on_trade {
|
||||
on_trade(self.selected_offer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,6 +121,7 @@ pub type PlayerFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
///
|
||||
/// Implementors are typically player entities that can open containers.
|
||||
pub trait InventoryPlayer: Send + Sync {
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
/// Drops an item into the world.
|
||||
///
|
||||
/// # Arguments
|
||||
@ -1059,6 +1060,56 @@ pub trait ScreenHandler: Send + Sync {
|
||||
let slot_stack = slot.get_cloned_stack().await;
|
||||
let mut cursor_stack = self.get_behaviour().cursor_stack.lock().await;
|
||||
|
||||
if click_type == MouseClick::Right {
|
||||
let mut intercepted = false;
|
||||
|
||||
if !cursor_stack.is_empty() {
|
||||
let stack_guard = slot.get_stack().await;
|
||||
let mut inner_slot_stack = stack_guard.lock().await;
|
||||
if let Some(bundle) = inner_slot_stack.get_data_component_mut::<pumpkin_data::data_component_impl::BundleContentsImpl>()
|
||||
&& bundle.try_insert(&mut cursor_stack) {
|
||||
intercepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !intercepted && !slot_stack.is_empty()
|
||||
&& let Some(bundle) = cursor_stack.get_data_component_mut::<pumpkin_data::data_component_impl::BundleContentsImpl>() {
|
||||
let stack_guard = slot.get_stack().await;
|
||||
let mut inner_slot_stack = stack_guard.lock().await;
|
||||
if bundle.try_insert(&mut inner_slot_stack) {
|
||||
if inner_slot_stack.item_count == 0 {
|
||||
*inner_slot_stack = ItemStack::EMPTY.clone();
|
||||
}
|
||||
intercepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !intercepted && cursor_stack.is_empty() {
|
||||
let stack_guard = slot.get_stack().await;
|
||||
let mut inner_slot_stack = stack_guard.lock().await;
|
||||
if let Some(bundle) = inner_slot_stack.get_data_component_mut::<pumpkin_data::data_component_impl::BundleContentsImpl>()
|
||||
&& let Some(extracted) = bundle.try_extract() {
|
||||
*cursor_stack = extracted;
|
||||
intercepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !intercepted && slot_stack.is_empty()
|
||||
&& let Some(bundle) = cursor_stack.get_data_component_mut::<pumpkin_data::data_component_impl::BundleContentsImpl>()
|
||||
&& let Some(extracted) = bundle.try_extract() {
|
||||
slot.set_stack(extracted).await;
|
||||
intercepted = true;
|
||||
}
|
||||
|
||||
if intercepted {
|
||||
if cursor_stack.item_count == 0 {
|
||||
*cursor_stack = ItemStack::EMPTY.clone();
|
||||
}
|
||||
slot.mark_dirty().await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let equipment_slot = cursor_stack
|
||||
.get_data_component::<EquippableImpl>()
|
||||
.map_or(&EquipmentSlot::MAIN_HAND, |equippable| equippable.slot);
|
||||
|
||||
@ -7,9 +7,9 @@ use crate::codec::item_stack_seralizer::ItemStackSerializer;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct MerchantOffer {
|
||||
pub base_cost_a: ItemStackSerializer<'static>,
|
||||
pub base_cost_a: ItemStackSerializer<'static>, // TODO: item cost
|
||||
pub output: ItemStackSerializer<'static>,
|
||||
pub cost_b: Option<ItemStackSerializer<'static>>,
|
||||
pub cost_b: Option<ItemStackSerializer<'static>>, // TODO: item cost
|
||||
pub is_disabled: bool,
|
||||
pub uses: i32,
|
||||
pub max_uses: i32,
|
||||
|
||||
@ -3,8 +3,8 @@ use std::sync::Arc;
|
||||
use crate::block::entities::lectern::LecternBlockEntity;
|
||||
use crate::block::registry::BlockActionResult;
|
||||
use crate::block::{
|
||||
BlockBehaviour, BlockFuture, BrokenArgs, NormalUseArgs, OnPlaceArgs, PlacedArgs,
|
||||
UseWithItemArgs,
|
||||
BlockBehaviour, BlockFuture, BrokenArgs, GetComparatorOutputArgs, NormalUseArgs, OnPlaceArgs,
|
||||
PlacedArgs, UseWithItemArgs,
|
||||
};
|
||||
use crate::entity::Entity;
|
||||
use crate::entity::item::ItemEntity;
|
||||
@ -148,4 +148,41 @@ impl BlockBehaviour for LecternBlock {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn get_comparator_output<'a>(
|
||||
&'a self,
|
||||
args: GetComparatorOutputArgs<'a>,
|
||||
) -> BlockFuture<'a, Option<u8>> {
|
||||
Box::pin(async move {
|
||||
if let Some(block_entity) = args.world.get_block_entity(args.position)
|
||||
&& let Some(lectern_entity) =
|
||||
block_entity.as_any().downcast_ref::<LecternBlockEntity>()
|
||||
{
|
||||
let book_guard = lectern_entity.book.lock().await;
|
||||
if book_guard.is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let page = lectern_entity
|
||||
.page
|
||||
.load(std::sync::atomic::Ordering::Relaxed) as f32;
|
||||
let mut pages = 1.0;
|
||||
if let Some(comp) = book_guard.get_data_component::<pumpkin_data::data_component_impl::WrittenBookContentImpl>() {
|
||||
pages = comp.pages.len().max(1) as f32;
|
||||
} else if let Some(comp) = book_guard.get_data_component::<pumpkin_data::data_component_impl::WritableBookContentImpl>() {
|
||||
pages = comp.pages.len().max(1) as f32;
|
||||
}
|
||||
|
||||
let output: f32 = if pages > 1.0 {
|
||||
1.0 + 14.0 * page / (pages - 1.0)
|
||||
} else {
|
||||
15.0
|
||||
};
|
||||
|
||||
Some(output.floor() as u8)
|
||||
} else {
|
||||
Some(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,11 @@ use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use std::{
|
||||
any::Any,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
@ -17,6 +18,7 @@ use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture};
|
||||
pub struct LecternBlockEntity {
|
||||
pub position: BlockPos,
|
||||
pub book: Arc<Mutex<ItemStack>>,
|
||||
pub page: AtomicUsize,
|
||||
pub dirty: AtomicBool,
|
||||
}
|
||||
|
||||
@ -41,9 +43,12 @@ impl BlockEntity for LecternBlockEntity {
|
||||
|stack| Arc::new(Mutex::new(stack)),
|
||||
);
|
||||
|
||||
let page = nbt.get_int("Page").unwrap_or(0).max(0) as usize;
|
||||
|
||||
Self {
|
||||
position,
|
||||
book,
|
||||
page: AtomicUsize::new(page),
|
||||
dirty: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
@ -59,6 +64,7 @@ impl BlockEntity for LecternBlockEntity {
|
||||
book.write_item_stack(&mut book_nbt);
|
||||
nbt.put_compound("Book", book_nbt);
|
||||
}
|
||||
nbt.put_int("Page", self.page.load(Ordering::Relaxed) as i32);
|
||||
})
|
||||
}
|
||||
|
||||
@ -87,6 +93,7 @@ impl LecternBlockEntity {
|
||||
Self {
|
||||
position,
|
||||
book: Arc::new(Mutex::new(ItemStack::EMPTY.clone())),
|
||||
page: AtomicUsize::new(0),
|
||||
dirty: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
@ -397,6 +397,14 @@ pub trait Mob: EntityBase + Send + Sync {
|
||||
|
||||
fn get_mob_entity(&self) -> &MobEntity;
|
||||
|
||||
fn get_job_site(&self) -> Option<BlockPos> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_home(&self) -> Option<BlockPos> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_path_aware_entity(&self) -> Option<&dyn PathAwareEntity> {
|
||||
None
|
||||
}
|
||||
@ -675,6 +683,14 @@ impl<T: Mob + Send + 'static> EntityBase for T {
|
||||
.is_some_and(PathAwareEntity::is_panicking)
|
||||
}
|
||||
|
||||
fn get_job_site_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
|
||||
<T as Mob>::get_job_site(self)
|
||||
}
|
||||
|
||||
fn get_home_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
|
||||
<T as Mob>::get_home(self)
|
||||
}
|
||||
|
||||
fn as_nbt_storage(&self) -> &dyn NBTStorage {
|
||||
self
|
||||
}
|
||||
|
||||
@ -147,6 +147,14 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any {
|
||||
})
|
||||
}
|
||||
|
||||
fn get_job_site_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_home_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any
|
||||
where
|
||||
Self: Sized,
|
||||
|
||||
@ -26,23 +26,30 @@ pub enum GossipType {
|
||||
Trading = 4,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct VillagerData {
|
||||
pub r#type: VillagerType,
|
||||
pub profession: VillagerProfession,
|
||||
pub level: i32,
|
||||
pub r#type: VarInt,
|
||||
pub profession: VarInt,
|
||||
pub level: VarInt,
|
||||
}
|
||||
|
||||
impl Serialize for VillagerData {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
let mut state = serializer.serialize_struct("VillagerData", 3)?;
|
||||
state.serialize_field("type", &VarInt(self.r#type as i32))?;
|
||||
state.serialize_field("profession", &VarInt(self.profession as i32))?;
|
||||
state.serialize_field("level", &VarInt(self.level))?;
|
||||
state.end()
|
||||
impl VillagerData {
|
||||
#[must_use]
|
||||
pub const fn new(r#type: VillagerType, profession: VillagerProfession, level: i32) -> Self {
|
||||
Self {
|
||||
r#type: VarInt(r#type as i32),
|
||||
profession: VarInt(profession as i32),
|
||||
level: VarInt(level),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn type_enum(&self) -> VillagerType {
|
||||
VillagerType::from_i32(self.r#type.0).unwrap_or(VillagerType::Plains)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn profession_enum(&self) -> VillagerProfession {
|
||||
VillagerProfession::from_i32(self.profession.0).unwrap_or(VillagerProfession::None)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,16 +3,24 @@ use std::sync::atomic::{AtomicI32, AtomicI64, Ordering};
|
||||
use std::sync::{Arc, Weak};
|
||||
use uuid::Uuid;
|
||||
|
||||
use pumpkin_data::entity::EntityType;
|
||||
use crate::block::blocks::bed::BedBlock;
|
||||
use pumpkin_data::Block;
|
||||
use pumpkin_data::block_properties::{
|
||||
BedPart, BlockProperties, WhiteBedLikeProperties as BedProperties,
|
||||
};
|
||||
use pumpkin_data::entity::{EntityPose, EntityType};
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_data::meta_data_type::MetaDataType;
|
||||
use pumpkin_data::tag::Taggable;
|
||||
use pumpkin_data::tracked_data::TrackedData;
|
||||
use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler;
|
||||
use pumpkin_inventory::screen_handler::{
|
||||
BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler,
|
||||
};
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_protocol::java::client::play::Metadata;
|
||||
use pumpkin_protocol::codec::var_int::VarInt;
|
||||
use pumpkin_protocol::java::client::play::{CMerchantOffers, Metadata};
|
||||
use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3};
|
||||
use pumpkin_util::text::TextComponent;
|
||||
use pumpkin_world::inventory::SimpleInventory;
|
||||
use tokio::sync::Mutex;
|
||||
@ -44,16 +52,15 @@ pub struct VillagerEntity {
|
||||
pub inventory: Arc<Mutex<Vec<Arc<Mutex<ItemStack>>>>>,
|
||||
pub merchant_inventory: Arc<SimpleInventory>,
|
||||
pub offers: Mutex<Vec<pumpkin_protocol::java::client::play::MerchantOffer>>,
|
||||
pub job_site: std::sync::Mutex<Option<BlockPos>>,
|
||||
pub home_pos: std::sync::Mutex<Option<BlockPos>>,
|
||||
pub self_weak: std::sync::Mutex<Option<Weak<Self>>>,
|
||||
}
|
||||
|
||||
impl VillagerEntity {
|
||||
pub fn new(entity: Entity) -> Arc<Self> {
|
||||
let mob_entity = MobEntity::new(entity);
|
||||
let villager_data = VillagerData {
|
||||
r#type: VillagerType::Plains,
|
||||
profession: VillagerProfession::None,
|
||||
level: 1,
|
||||
};
|
||||
let villager_data = VillagerData::new(VillagerType::Plains, VillagerProfession::None, 1);
|
||||
let inventory = Arc::new(Mutex::new(
|
||||
(0..8)
|
||||
.map(|_| Arc::new(Mutex::new(ItemStack::EMPTY.clone())))
|
||||
@ -71,8 +78,12 @@ impl VillagerEntity {
|
||||
inventory,
|
||||
merchant_inventory: Arc::new(SimpleInventory::new(3)),
|
||||
offers: Mutex::new(Vec::new()),
|
||||
job_site: std::sync::Mutex::new(None),
|
||||
home_pos: std::sync::Mutex::new(None),
|
||||
self_weak: std::sync::Mutex::new(None),
|
||||
};
|
||||
let mob_arc = Arc::new(villager);
|
||||
*mob_arc.self_weak.lock().unwrap() = Some(Arc::downgrade(&mob_arc));
|
||||
let mob_weak: Weak<dyn Mob> = {
|
||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
||||
Arc::downgrade(&mob_arc)
|
||||
@ -193,9 +204,12 @@ impl VillagerEntity {
|
||||
}
|
||||
|
||||
pub async fn set_villager_data(&self, data: VillagerData) {
|
||||
let old_profession = {
|
||||
let mut villager_data = self.villager_data.lock().await;
|
||||
let old_profession = villager_data.profession;
|
||||
*villager_data = data;
|
||||
old_profession
|
||||
};
|
||||
self.get_entity().send_meta_data(&[Metadata::new(
|
||||
TrackedData::VILLAGER_DATA,
|
||||
MetaDataType::VILLAGER_DATA,
|
||||
@ -203,20 +217,20 @@ impl VillagerEntity {
|
||||
)]);
|
||||
|
||||
if old_profession != data.profession {
|
||||
self.generate_trades(data.profession, data.level).await;
|
||||
if let Some(sound) = data.profession.work_sound() {
|
||||
self.generate_trades(data.profession_enum(), data.level.0)
|
||||
.await;
|
||||
if let Some(sound) = data.profession_enum().work_sound() {
|
||||
self.get_entity().play_sound(sound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_trades(&self, profession: VillagerProfession, level: i32) {
|
||||
pub async fn add_trades(&self, profession: VillagerProfession, level: i32) {
|
||||
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
|
||||
use rand::seq::IndexedRandom;
|
||||
use std::borrow::Cow;
|
||||
|
||||
let mut offers = self.offers.lock().await;
|
||||
offers.clear();
|
||||
|
||||
if let Some(trade_set) = profession.trade_set(level) {
|
||||
let mut rng = rand::rng();
|
||||
@ -247,6 +261,11 @@ impl VillagerEntity {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_trades(&self, profession: VillagerProfession, level: i32) {
|
||||
self.offers.lock().await.clear();
|
||||
self.add_trades(profession, level).await;
|
||||
}
|
||||
|
||||
pub fn set_unhappy(&self) {
|
||||
let entity = self.get_entity();
|
||||
entity
|
||||
@ -256,23 +275,27 @@ impl VillagerEntity {
|
||||
entity.play_sound(pumpkin_data::sound::Sound::EntityVillagerNo);
|
||||
}
|
||||
|
||||
pub const fn open_trading_screen(&self, _player: &Arc<Player>) {
|
||||
// let self_weak = self.self_arc.lock().await;
|
||||
// if let Some(self_arc) = self_weak.as_ref().and_then(std::sync::Weak::upgrade) {
|
||||
// player.open_handled_screen(&*self_arc, None);
|
||||
pub async fn open_trading_screen(&self, player: &Arc<Player>) {
|
||||
use pumpkin_protocol::codec::var_int::VarInt;
|
||||
use pumpkin_protocol::java::client::play::CMerchantOffers;
|
||||
|
||||
// let offers = self.offers.lock().await;
|
||||
// let villager_data = self.villager_data.lock().await;
|
||||
// Open the merchant screen and then send the current offers packet
|
||||
if let Some(sync_id) = player.open_handled_screen(self, None).await {
|
||||
let offers = self.offers.lock().await.clone();
|
||||
let villager_data = self.villager_data.lock().await;
|
||||
|
||||
// player.client.enqueue_packet(&CMerchantOffers::new(
|
||||
// player.screen_handler_sync_id.load(Ordering::Relaxed).into(),
|
||||
// offers.clone(),
|
||||
// VarInt(villager_data.level),
|
||||
// VarInt(self.xp.load(Ordering::Relaxed)),
|
||||
// true,
|
||||
// true,
|
||||
// ));
|
||||
// }
|
||||
player
|
||||
.client
|
||||
.enqueue_packet(&CMerchantOffers::new(
|
||||
VarInt(sync_id as i32),
|
||||
offers,
|
||||
villager_data.level,
|
||||
VarInt(self.xp.load(Ordering::Relaxed)),
|
||||
true,
|
||||
true,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,17 +304,92 @@ impl ScreenHandlerFactory for VillagerEntity {
|
||||
&'a self,
|
||||
sync_id: u8,
|
||||
player_inventory: &'a Arc<pumpkin_inventory::player::player_inventory::PlayerInventory>,
|
||||
_player: &'a dyn InventoryPlayer,
|
||||
player: &'a dyn InventoryPlayer,
|
||||
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
|
||||
Box::pin(async move {
|
||||
let offers = self.offers.lock().await;
|
||||
let handler = MerchantScreenHandler::new(
|
||||
let self_weak = self.self_weak.lock().unwrap().clone().unwrap();
|
||||
let player_uuid = player
|
||||
.as_any()
|
||||
.downcast_ref::<crate::entity::player::Player>()
|
||||
.map_or_else(uuid::Uuid::nil, |p| p.get_entity().entity_uuid);
|
||||
let world = self.get_entity().world.load().clone();
|
||||
|
||||
let mut handler = MerchantScreenHandler::new(
|
||||
sync_id,
|
||||
player_inventory,
|
||||
self.merchant_inventory.clone(),
|
||||
offers.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
handler.on_trade = Some(Box::new(move |offer_index| {
|
||||
if let Some(villager) = self_weak.upgrade() {
|
||||
let world = world.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Some(player) = world.get_player_by_uuid(player_uuid) {
|
||||
let mut offers = villager.offers.lock().await;
|
||||
if offer_index < offers.len() {
|
||||
let offer = &mut offers[offer_index];
|
||||
offer.uses += 1;
|
||||
|
||||
let xp_gain = offer.xp;
|
||||
let current_xp =
|
||||
villager.xp.fetch_add(xp_gain, Ordering::Relaxed) + xp_gain;
|
||||
|
||||
let mut data = villager.villager_data.lock().await;
|
||||
let current_level = data.level.0;
|
||||
if current_level < 5 {
|
||||
let max_xp = match current_level {
|
||||
1 => 10,
|
||||
2 => 70,
|
||||
3 => 150,
|
||||
4 => 250,
|
||||
_ => 0,
|
||||
};
|
||||
if current_xp >= max_xp {
|
||||
data.level.0 += 1;
|
||||
let new_level = data.level.0;
|
||||
let prof = data.profession_enum();
|
||||
drop(data);
|
||||
|
||||
// Level up! Add new trades for the new level
|
||||
villager.add_trades(prof, new_level).await;
|
||||
|
||||
// Play sound & particles for level up!
|
||||
let entity = villager.get_entity();
|
||||
entity.world.load().send_entity_status(
|
||||
entity,
|
||||
pumpkin_data::entity::EntityStatus::VillagerHappy,
|
||||
);
|
||||
entity.play_sound(
|
||||
pumpkin_data::sound::Sound::EntityVillagerCelebrate,
|
||||
);
|
||||
} else {
|
||||
drop(data);
|
||||
}
|
||||
} else {
|
||||
drop(data);
|
||||
}
|
||||
|
||||
let current_level = villager.villager_data.lock().await.level;
|
||||
player
|
||||
.client
|
||||
.enqueue_packet(&CMerchantOffers::new(
|
||||
VarInt(sync_id as i32),
|
||||
offers.clone(),
|
||||
current_level,
|
||||
VarInt(current_xp),
|
||||
true,
|
||||
true,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
Some(Arc::new(Mutex::new(handler)) as SharedScreenHandler)
|
||||
})
|
||||
}
|
||||
@ -308,9 +406,9 @@ impl NBTStorage for VillagerEntity {
|
||||
self.mob_entity.living_entity.entity.write_nbt(nbt).await;
|
||||
let data = self.villager_data.lock().await;
|
||||
let mut villager_data_nbt = NbtCompound::new();
|
||||
villager_data_nbt.put_int("Type", data.r#type as i32);
|
||||
villager_data_nbt.put_int("Profession", data.profession as i32);
|
||||
villager_data_nbt.put_int("Level", data.level);
|
||||
villager_data_nbt.put_int("Type", data.r#type.0);
|
||||
villager_data_nbt.put_int("Profession", data.profession.0);
|
||||
villager_data_nbt.put_int("Level", data.level.0);
|
||||
nbt.put_compound("VillagerData", villager_data_nbt);
|
||||
|
||||
nbt.put_int("FoodLevel", self.food_level.load(Ordering::Relaxed));
|
||||
@ -321,6 +419,58 @@ impl NBTStorage for VillagerEntity {
|
||||
);
|
||||
nbt.put_int("RestocksToday", self.restocks_today.load(Ordering::Relaxed));
|
||||
|
||||
let job_site_pos = *self.job_site.lock().unwrap();
|
||||
if let Some(pos) = job_site_pos {
|
||||
nbt.put_int("JobSiteX", pos.0.x);
|
||||
nbt.put_int("JobSiteY", pos.0.y);
|
||||
nbt.put_int("JobSiteZ", pos.0.z);
|
||||
}
|
||||
|
||||
let home_pos = *self.home_pos.lock().unwrap();
|
||||
if let Some(pos) = home_pos {
|
||||
nbt.put_int("HomeX", pos.0.x);
|
||||
nbt.put_int("HomeY", pos.0.y);
|
||||
nbt.put_int("HomeZ", pos.0.z);
|
||||
}
|
||||
|
||||
// Save Offers
|
||||
{
|
||||
let offers = self.offers.lock().await;
|
||||
let mut recipes = Vec::new();
|
||||
for offer in offers.iter() {
|
||||
let mut recipe = NbtCompound::new();
|
||||
|
||||
let mut buy = NbtCompound::new();
|
||||
offer.base_cost_a.0.write_item_stack(&mut buy);
|
||||
recipe.put_compound("buy", buy);
|
||||
|
||||
if let Some(cost_b) = &offer.cost_b
|
||||
&& !cost_b.0.is_empty()
|
||||
{
|
||||
let mut buy_b = NbtCompound::new();
|
||||
cost_b.0.write_item_stack(&mut buy_b);
|
||||
recipe.put_compound("buyB", buy_b);
|
||||
}
|
||||
|
||||
let mut sell_item = NbtCompound::new();
|
||||
offer.output.0.write_item_stack(&mut sell_item);
|
||||
recipe.put_compound("sell", sell_item);
|
||||
|
||||
recipe.put_int("uses", offer.uses);
|
||||
recipe.put_int("maxUses", offer.max_uses);
|
||||
recipe.put_bool("rewardExp", !offer.is_disabled);
|
||||
recipe.put_int("xp", offer.xp);
|
||||
recipe.put_float("priceMultiplier", offer.price_multiplier);
|
||||
recipe.put_int("specialPrice", offer.special_price);
|
||||
recipe.put_int("demand", offer.demand);
|
||||
|
||||
recipes.push(pumpkin_nbt::tag::NbtTag::Compound(recipe));
|
||||
}
|
||||
let mut offers_compound = NbtCompound::new();
|
||||
offers_compound.put("Recipes", pumpkin_nbt::tag::NbtTag::List(recipes));
|
||||
nbt.put_compound("Offers", offers_compound);
|
||||
};
|
||||
|
||||
// Inventory
|
||||
let inventory = self.inventory.lock().await;
|
||||
let mut inventory_list = Vec::new();
|
||||
@ -359,6 +509,7 @@ impl NBTStorage for VillagerEntity {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> crate::entity::NbtFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
self.mob_entity
|
||||
@ -369,14 +520,13 @@ impl NBTStorage for VillagerEntity {
|
||||
if let Some(villager_data_nbt) = nbt.get_compound("VillagerData") {
|
||||
let mut data = self.villager_data.lock().await;
|
||||
if let Some(t) = villager_data_nbt.get_int("Type") {
|
||||
data.r#type = VillagerType::try_from(t).unwrap_or(VillagerType::Plains);
|
||||
data.r#type = VarInt(t);
|
||||
}
|
||||
if let Some(p) = villager_data_nbt.get_int("Profession") {
|
||||
data.profession =
|
||||
VillagerProfession::try_from(p).unwrap_or(VillagerProfession::None);
|
||||
data.profession = VarInt(p);
|
||||
}
|
||||
if let Some(l) = villager_data_nbt.get_int("Level") {
|
||||
data.level = l;
|
||||
data.level = VarInt(l);
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,6 +543,70 @@ impl NBTStorage for VillagerEntity {
|
||||
self.restocks_today.store(today, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if let (Some(x), Some(y), Some(z)) = (
|
||||
nbt.get_int("JobSiteX"),
|
||||
nbt.get_int("JobSiteY"),
|
||||
nbt.get_int("JobSiteZ"),
|
||||
) {
|
||||
*self.job_site.lock().unwrap() = Some(BlockPos::new(x, y, z));
|
||||
} else {
|
||||
*self.job_site.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
if let (Some(x), Some(y), Some(z)) = (
|
||||
nbt.get_int("HomeX").or_else(|| nbt.get_int("BedX")),
|
||||
nbt.get_int("HomeY").or_else(|| nbt.get_int("BedY")),
|
||||
nbt.get_int("HomeZ").or_else(|| nbt.get_int("BedZ")),
|
||||
) {
|
||||
*self.home_pos.lock().unwrap() = Some(BlockPos::new(x, y, z));
|
||||
} else {
|
||||
*self.home_pos.lock().unwrap() = None;
|
||||
}
|
||||
|
||||
if let Some(offers_compound) = nbt.get_compound("Offers")
|
||||
&& let Some(recipes) = offers_compound.get_list("Recipes")
|
||||
{
|
||||
let mut offers = self.offers.lock().await;
|
||||
offers.clear();
|
||||
for tag in recipes {
|
||||
if let Some(recipe) = tag.extract_compound() {
|
||||
let buy = recipe
|
||||
.get_compound("buy")
|
||||
.and_then(ItemStack::read_item_stack);
|
||||
let buy_b = recipe
|
||||
.get_compound("buyB")
|
||||
.and_then(ItemStack::read_item_stack);
|
||||
let sell_item = recipe
|
||||
.get_compound("sell")
|
||||
.and_then(ItemStack::read_item_stack);
|
||||
|
||||
if let (Some(buy), Some(sell_item)) = (buy, sell_item) {
|
||||
let uses = recipe.get_int("uses").unwrap_or(0);
|
||||
let max_uses = recipe.get_int("maxUses").unwrap_or(12);
|
||||
let reward_exp = recipe.get_bool("rewardExp").unwrap_or(true);
|
||||
let xp = recipe.get_int("xp").unwrap_or(2);
|
||||
let price_multiplier =
|
||||
recipe.get_float("priceMultiplier").unwrap_or(0.05);
|
||||
let special_price = recipe.get_int("specialPrice").unwrap_or(0);
|
||||
let demand = recipe.get_int("demand").unwrap_or(0);
|
||||
|
||||
offers.push(pumpkin_protocol::java::client::play::MerchantOffer {
|
||||
base_cost_a: buy.into(),
|
||||
output: sell_item.into(),
|
||||
cost_b: buy_b.map(Into::into),
|
||||
is_disabled: !reward_exp,
|
||||
uses,
|
||||
max_uses,
|
||||
xp,
|
||||
special_price,
|
||||
price_multiplier,
|
||||
demand,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inventory
|
||||
if let Some(inventory_list) = nbt.get_list("Inventory") {
|
||||
let mut inventory = self.inventory.lock().await;
|
||||
@ -442,11 +656,349 @@ impl NBTStorage for VillagerEntity {
|
||||
}
|
||||
}
|
||||
|
||||
fn block_to_profession(block: &Block) -> Option<VillagerProfession> {
|
||||
if block == &Block::COMPOSTER {
|
||||
Some(VillagerProfession::Farmer)
|
||||
} else if block == &Block::LECTERN {
|
||||
Some(VillagerProfession::Librarian)
|
||||
} else if block == &Block::BLAST_FURNACE {
|
||||
Some(VillagerProfession::Armorer)
|
||||
} else if block == &Block::SMOKER {
|
||||
Some(VillagerProfession::Butcher)
|
||||
} else if block == &Block::CARTOGRAPHY_TABLE {
|
||||
Some(VillagerProfession::Cartographer)
|
||||
} else if block == &Block::BREWING_STAND {
|
||||
Some(VillagerProfession::Cleric)
|
||||
} else if block == &Block::BARREL {
|
||||
Some(VillagerProfession::Fisherman)
|
||||
} else if block == &Block::FLETCHING_TABLE {
|
||||
Some(VillagerProfession::Fletcher)
|
||||
} else if block == &Block::CAULDRON
|
||||
|| block == &Block::WATER_CAULDRON
|
||||
|| block == &Block::LAVA_CAULDRON
|
||||
|| block == &Block::POWDER_SNOW_CAULDRON
|
||||
{
|
||||
Some(VillagerProfession::Leatherworker)
|
||||
} else if block == &Block::STONECUTTER {
|
||||
Some(VillagerProfession::Mason)
|
||||
} else if block == &Block::LOOM {
|
||||
Some(VillagerProfession::Shepherd)
|
||||
} else if block == &Block::SMITHING_TABLE {
|
||||
Some(VillagerProfession::Toolsmith)
|
||||
} else if block == &Block::GRINDSTONE {
|
||||
Some(VillagerProfession::Weaponsmith)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn profession_matches_block(profession: VillagerProfession, block: &Block) -> bool {
|
||||
match profession {
|
||||
VillagerProfession::Farmer => block == &Block::COMPOSTER,
|
||||
VillagerProfession::Librarian => block == &Block::LECTERN,
|
||||
VillagerProfession::Armorer => block == &Block::BLAST_FURNACE,
|
||||
VillagerProfession::Butcher => block == &Block::SMOKER,
|
||||
VillagerProfession::Cartographer => block == &Block::CARTOGRAPHY_TABLE,
|
||||
VillagerProfession::Cleric => block == &Block::BREWING_STAND,
|
||||
VillagerProfession::Fisherman => block == &Block::BARREL,
|
||||
VillagerProfession::Fletcher => block == &Block::FLETCHING_TABLE,
|
||||
VillagerProfession::Leatherworker => {
|
||||
block == &Block::CAULDRON
|
||||
|| block == &Block::WATER_CAULDRON
|
||||
|| block == &Block::LAVA_CAULDRON
|
||||
|| block == &Block::POWDER_SNOW_CAULDRON
|
||||
}
|
||||
VillagerProfession::Mason => block == &Block::STONECUTTER,
|
||||
VillagerProfession::Shepherd => block == &Block::LOOM,
|
||||
VillagerProfession::Toolsmith => block == &Block::SMITHING_TABLE,
|
||||
VillagerProfession::Weaponsmith => block == &Block::GRINDSTONE,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
impl Mob for VillagerEntity {
|
||||
fn get_mob_entity(&self) -> &MobEntity {
|
||||
&self.mob_entity
|
||||
}
|
||||
|
||||
fn get_job_site(&self) -> Option<BlockPos> {
|
||||
*self.job_site.lock().unwrap()
|
||||
}
|
||||
|
||||
fn get_home(&self) -> Option<BlockPos> {
|
||||
*self.home_pos.lock().unwrap()
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
fn mob_tick<'a>(
|
||||
&'a self,
|
||||
_caller: &'a Arc<dyn EntityBase>,
|
||||
) -> crate::entity::EntityBaseFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
let age = self.get_entity().age.load(Ordering::Relaxed);
|
||||
if age % 20 != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let world = self.get_entity().world.load();
|
||||
|
||||
// 1. Bed / Sleeping logic (for all villagers: babies, nitwits, adults)
|
||||
let is_sleeping = self.get_entity().pose.load() == EntityPose::Sleeping;
|
||||
|
||||
// Check if current bed is still valid
|
||||
if let Some(current_home) = self.get_home_pos() {
|
||||
let (block, state) = world.get_block_and_state(¤t_home);
|
||||
let valid = if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_BEDS) {
|
||||
let bed_props = BedProperties::from_state_id(state.id, block);
|
||||
bed_props.part == BedPart::Head
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !valid {
|
||||
*self.home_pos.lock().unwrap() = None;
|
||||
if is_sleeping {
|
||||
// Wake up if bed was broken
|
||||
self.get_entity().set_pose(EntityPose::Standing);
|
||||
self.get_entity().send_meta_data(&[Metadata::new(
|
||||
TrackedData::SLEEPING_POS_ID,
|
||||
MetaDataType::OPTIONAL_BLOCK_POS,
|
||||
None::<BlockPos>,
|
||||
)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no bed, search for one
|
||||
if self.get_home_pos().is_none() {
|
||||
let pos = self.get_entity().block_pos.load();
|
||||
let start = BlockPos::new(pos.0.x - 16, pos.0.y - 4, pos.0.z - 16);
|
||||
let end = BlockPos::new(pos.0.x + 16, pos.0.y + 4, pos.0.z + 16);
|
||||
|
||||
let aabb = BoundingBox::new(
|
||||
Vector3::new(
|
||||
pos.0.x as f64 - 32.0,
|
||||
pos.0.y as f64 - 16.0,
|
||||
pos.0.z as f64 - 32.0,
|
||||
),
|
||||
Vector3::new(
|
||||
pos.0.x as f64 + 32.0,
|
||||
pos.0.y as f64 + 16.0,
|
||||
pos.0.z as f64 + 32.0,
|
||||
),
|
||||
);
|
||||
let nearby_entities = world.get_all_at_box(&aabb);
|
||||
|
||||
let mut claimed_homes = Vec::new();
|
||||
for entity in nearby_entities {
|
||||
if entity.get_entity().entity_id != self.get_entity().entity_id
|
||||
&& entity.get_entity().entity_type
|
||||
== &pumpkin_data::entity::EntityType::VILLAGER
|
||||
&& let Some(home) = entity.get_home_pos()
|
||||
{
|
||||
claimed_homes.push(home);
|
||||
}
|
||||
}
|
||||
|
||||
let mut best_home = None;
|
||||
let mut best_dist = f64::MAX;
|
||||
|
||||
for p in BlockPos::iterate(start, end) {
|
||||
let (block, state) = world.get_block_and_state(&p);
|
||||
if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_BEDS) {
|
||||
let bed_props = BedProperties::from_state_id(state.id, block);
|
||||
let bed_head_pos = if bed_props.part == BedPart::Head {
|
||||
p
|
||||
} else {
|
||||
p.offset(bed_props.facing.to_offset())
|
||||
};
|
||||
|
||||
if claimed_homes.contains(&bed_head_pos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dist = bed_head_pos
|
||||
.to_f64()
|
||||
.squared_distance_to_vec(&self.get_entity().pos.load());
|
||||
if dist < best_dist {
|
||||
best_dist = dist;
|
||||
best_home = Some(bed_head_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(home) = best_home {
|
||||
*self.home_pos.lock().unwrap() = Some(home);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Sleeping/Waking up based on time
|
||||
let is_sleeping = self.get_entity().pose.load() == EntityPose::Sleeping;
|
||||
if let Some(home_pos) = self.get_home_pos() {
|
||||
let time = world.level_time.lock().await.time_of_day;
|
||||
let is_night = (12000..=23000).contains(&time);
|
||||
|
||||
if is_night {
|
||||
if !is_sleeping {
|
||||
// Check distance to bed. If close enough, go to sleep
|
||||
let dist = home_pos
|
||||
.to_f64()
|
||||
.squared_distance_to_vec(&self.get_entity().pos.load());
|
||||
if dist <= 4.0 {
|
||||
// Within 2 blocks (squared distance 4.0)
|
||||
let (block, state) = world.get_block_and_state(&home_pos);
|
||||
if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_BEDS) {
|
||||
let bed_props = BedProperties::from_state_id(state.id, block);
|
||||
if !bed_props.occupied {
|
||||
// Make bed occupied
|
||||
BedBlock::set_occupied(
|
||||
true, &world, block, &home_pos, state.id,
|
||||
)
|
||||
.await;
|
||||
|
||||
self.get_entity().set_pose(EntityPose::Sleeping);
|
||||
self.get_entity().send_meta_data(&[Metadata::new(
|
||||
TrackedData::SLEEPING_POS_ID,
|
||||
MetaDataType::OPTIONAL_BLOCK_POS,
|
||||
Some(home_pos),
|
||||
)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if is_sleeping {
|
||||
// It is day, wake up!
|
||||
let (block, state) = world.get_block_and_state(&home_pos);
|
||||
if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_BEDS) {
|
||||
let bed_props = BedProperties::from_state_id(state.id, block);
|
||||
if bed_props.occupied {
|
||||
BedBlock::set_occupied(false, &world, block, &home_pos, state.id).await;
|
||||
}
|
||||
}
|
||||
|
||||
self.get_entity().set_pose(EntityPose::Standing);
|
||||
self.get_entity().send_meta_data(&[Metadata::new(
|
||||
TrackedData::SLEEPING_POS_ID,
|
||||
MetaDataType::OPTIONAL_BLOCK_POS,
|
||||
None::<BlockPos>,
|
||||
)]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Job / Profession logic (skip for Nitwits and babies)
|
||||
let data = self.villager_data.lock().await;
|
||||
let is_adult = self.get_entity().age.load(Ordering::Relaxed) >= 0;
|
||||
let xp = self.xp.load(Ordering::Relaxed);
|
||||
let profession = data.profession_enum();
|
||||
drop(data);
|
||||
|
||||
if profession == VillagerProfession::Nitwit || !is_adult {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(current_site) = self.get_job_site() {
|
||||
let (block, _state) = world.get_block_and_state(¤t_site);
|
||||
let valid = if profession == VillagerProfession::None {
|
||||
block_to_profession(block).is_some()
|
||||
} else {
|
||||
profession_matches_block(profession, block)
|
||||
};
|
||||
|
||||
if !valid {
|
||||
*self.job_site.lock().unwrap() = None;
|
||||
if xp == 0 && profession != VillagerProfession::None {
|
||||
let r#type = self.villager_data.lock().await.type_enum();
|
||||
self.set_villager_data(VillagerData::new(
|
||||
r#type,
|
||||
VillagerProfession::None,
|
||||
1,
|
||||
))
|
||||
.await;
|
||||
self.offers.lock().await.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.get_job_site().is_none() {
|
||||
let pos = self.get_entity().block_pos.load();
|
||||
let start = BlockPos::new(pos.0.x - 10, pos.0.y - 4, pos.0.z - 10);
|
||||
let end = BlockPos::new(pos.0.x + 10, pos.0.y + 4, pos.0.z + 10);
|
||||
|
||||
let aabb = BoundingBox::new(
|
||||
Vector3::new(
|
||||
pos.0.x as f64 - 32.0,
|
||||
pos.0.y as f64 - 16.0,
|
||||
pos.0.z as f64 - 32.0,
|
||||
),
|
||||
Vector3::new(
|
||||
pos.0.x as f64 + 32.0,
|
||||
pos.0.y as f64 + 16.0,
|
||||
pos.0.z as f64 + 32.0,
|
||||
),
|
||||
);
|
||||
let nearby_entities = world.get_all_at_box(&aabb);
|
||||
|
||||
let mut claimed_sites = Vec::new();
|
||||
for entity in nearby_entities {
|
||||
if entity.get_entity().entity_id != self.get_entity().entity_id
|
||||
&& entity.get_entity().entity_type
|
||||
== &pumpkin_data::entity::EntityType::VILLAGER
|
||||
&& let Some(site) = entity.get_job_site_pos()
|
||||
{
|
||||
claimed_sites.push(site);
|
||||
}
|
||||
}
|
||||
|
||||
let mut best_site = None;
|
||||
let mut best_dist = f64::MAX;
|
||||
let mut best_profession = VillagerProfession::None;
|
||||
|
||||
for p in BlockPos::iterate(start, end) {
|
||||
if claimed_sites.contains(&p) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (block, _state) = world.get_block_and_state(&p);
|
||||
if let Some(prof) = block_to_profession(block) {
|
||||
if profession != VillagerProfession::None && prof != profession {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dist = p
|
||||
.to_f64()
|
||||
.squared_distance_to_vec(&self.get_entity().pos.load());
|
||||
if dist < best_dist {
|
||||
best_dist = dist;
|
||||
best_site = Some(p);
|
||||
best_profession = prof;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(site) = best_site {
|
||||
*self.job_site.lock().unwrap() = Some(site);
|
||||
if profession == VillagerProfession::None {
|
||||
let r#type = self.villager_data.lock().await.type_enum();
|
||||
self.set_villager_data(VillagerData::new(r#type, best_profession, 1))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let current_prof = self.villager_data.lock().await.profession_enum();
|
||||
if current_prof == VillagerProfession::None
|
||||
&& let Some(site) = self.get_job_site()
|
||||
{
|
||||
let (block, _state) = world.get_block_and_state(&site);
|
||||
if let Some(prof) = block_to_profession(block) {
|
||||
let r#type = self.villager_data.lock().await.type_enum();
|
||||
self.set_villager_data(VillagerData::new(r#type, prof, 1))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn mob_interact<'a>(
|
||||
&'a self,
|
||||
player: &'a Arc<Player>,
|
||||
@ -459,7 +1011,23 @@ impl Mob for VillagerEntity {
|
||||
return true;
|
||||
}
|
||||
|
||||
let offers = self.offers.lock().await;
|
||||
let mut offers = self.offers.lock().await;
|
||||
if offers.is_empty() {
|
||||
let data = self.villager_data.lock().await;
|
||||
if data.profession_enum() != VillagerProfession::None
|
||||
&& data.profession_enum() != VillagerProfession::Nitwit
|
||||
{
|
||||
let prof = data.profession_enum();
|
||||
let level = data.level.0;
|
||||
drop(data);
|
||||
drop(offers);
|
||||
self.generate_trades(prof, level).await;
|
||||
offers = self.offers.lock().await;
|
||||
} else {
|
||||
drop(data);
|
||||
}
|
||||
}
|
||||
|
||||
if offers.is_empty() {
|
||||
self.set_unhappy();
|
||||
return true;
|
||||
@ -474,7 +1042,7 @@ impl Mob for VillagerEntity {
|
||||
)
|
||||
.await;
|
||||
|
||||
self.open_trading_screen(&player);
|
||||
self.open_trading_screen(&player).await;
|
||||
|
||||
true
|
||||
})
|
||||
|
||||
@ -4796,6 +4796,10 @@ impl MessageCache {
|
||||
}
|
||||
|
||||
impl InventoryPlayer for Player {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn drop_item(&self, item: ItemStack, _retain_ownership: bool) -> PlayerFuture<'_, ()> {
|
||||
Box::pin(async move {
|
||||
self.drop_item(item).await;
|
||||
|
||||
@ -150,15 +150,13 @@ impl EntityBase for MinecartEntity {
|
||||
&pos,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if self.vehicle.get_hurt_time() == 0 {
|
||||
} else if self.vehicle.get_hurt_time() == 0 {
|
||||
self.vehicle.set_hurt_dir(-self.vehicle.get_hurt_dir());
|
||||
self.vehicle.set_hurt_time(10);
|
||||
self.vehicle.set_damage(50.0);
|
||||
// TODO: Send entity status
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if is_powered_rail {
|
||||
let mut velocity = self.vehicle.entity.velocity.load();
|
||||
velocity = velocity.multiply(0.5, 0.5, 0.5);
|
||||
|
||||
@ -1775,13 +1775,15 @@ impl JavaClient {
|
||||
}
|
||||
ActionType::Interact | ActionType::InteractAt => {
|
||||
let held = player.inventory.held_item();
|
||||
let mut stack = held.lock().await;
|
||||
if !event.target.interact(player, &mut stack).await {
|
||||
let mut stack = held.lock().await.clone();
|
||||
let interacted = event.target.interact(player, &mut stack).await;
|
||||
if !interacted {
|
||||
server
|
||||
.item_registry
|
||||
.use_on_entity(&mut stack, player, event.target)
|
||||
.await;
|
||||
}
|
||||
*held.lock().await = stack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user