diff --git a/pumpkin-world/src/loot/condition/mod.rs b/pumpkin-world/src/loot/condition/mod.rs new file mode 100644 index 000000000..1c9002fd0 --- /dev/null +++ b/pumpkin-world/src/loot/condition/mod.rs @@ -0,0 +1,57 @@ +use serde::Deserialize; + +mod survives_explosion; + +#[derive(Deserialize, Clone)] +#[serde(tag = "condition")] +pub enum LootCondition { + #[serde(rename = "minecraft:inverted")] + Inverted, + #[serde(rename = "minecraft:any_of")] + AnyOf, + #[serde(rename = "minecraft:all_of")] + AllOf, + #[serde(rename = "minecraft:random_chance")] + RandomChance, + #[serde(rename = "minecraft:random_chance_with_enchanted_bonus")] + RandomChanceWithEnchantedBonus, + #[serde(rename = "minecraft:entity_properties")] + EntityProperties, + #[serde(rename = "minecraft:killed_by_player")] + KilledByPlayer, + #[serde(rename = "minecraft:entity_scores")] + EntityScores, + #[serde(rename = "minecraft:block_state_property")] + BlockStateProperty, + #[serde(rename = "minecraft:match_tool")] + MatchTool, + #[serde(rename = "minecraft:table_bonus")] + TableBonus, + #[serde(rename = "minecraft:survives_explosion")] + SurvivesExplosion, + #[serde(rename = "minecraft:damage_source_properties")] + DamageSourceProperties, + #[serde(rename = "minecraft:location_check")] + LocationCheck, + #[serde(rename = "minecraft:weather_check")] + WeatherCheck, + #[serde(rename = "minecraft:reference")] + Reference, + #[serde(rename = "minecraft:time_check")] + TimeCheck, + #[serde(rename = "minecraft:value_check")] + ValueCheck, + #[serde(rename = "minecraft:enchantment_active_check")] + EnchantmentActiveCheck, +} + +#[expect(clippy::match_like_matches_macro)] +impl LootCondition { + // TODO: This is trash, Make this right + pub fn test(&self) -> bool { + match self { + LootCondition::SurvivesExplosion => true, + _ => false, + } + } +} diff --git a/pumpkin-world/src/loot/condition/survives_explosion.rs b/pumpkin-world/src/loot/condition/survives_explosion.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/pumpkin-world/src/loot/condition/survives_explosion.rs @@ -0,0 +1 @@ + diff --git a/pumpkin-world/src/loot/entry/alternative.rs b/pumpkin-world/src/loot/entry/alternative.rs new file mode 100644 index 000000000..46a5b803e --- /dev/null +++ b/pumpkin-world/src/loot/entry/alternative.rs @@ -0,0 +1,30 @@ +use serde::Deserialize; + +use crate::{item::ItemStack, loot::LootPoolEntry}; + +#[derive(Deserialize, Clone)] +pub struct AlternativeEntry { + children: Vec, +} + +impl AlternativeEntry { + pub fn get_items(&self) -> Vec { + let mut items = vec![]; + for child in &self.children { + if let Some(conditions) = &child.conditions { + let mut conditions_met = true; + for condition in conditions { + if !condition.test() { + conditions_met = false; + break; + } + } + if !conditions_met { + continue; + } + } + items.extend_from_slice(&child.content.get_items()); + } + items + } +} diff --git a/pumpkin-world/src/loot/entry/item.rs b/pumpkin-world/src/loot/entry/item.rs new file mode 100644 index 000000000..60f06fe62 --- /dev/null +++ b/pumpkin-world/src/loot/entry/item.rs @@ -0,0 +1,16 @@ +use pumpkin_data::item::Item; +use serde::Deserialize; + +use crate::item::ItemStack; + +#[derive(Deserialize, Clone)] +pub struct ItemEntry { + name: String, +} + +impl ItemEntry { + pub fn get_items(&self) -> Vec { + let item = Item::from_registry_key(&self.name.replace("minecraft:", "")).unwrap(); + vec![ItemStack::new(1, item)] + } +} diff --git a/pumpkin-world/src/loot/entry/mod.rs b/pumpkin-world/src/loot/entry/mod.rs new file mode 100644 index 000000000..3360addf2 --- /dev/null +++ b/pumpkin-world/src/loot/entry/mod.rs @@ -0,0 +1,44 @@ +use alternative::AlternativeEntry; +use item::ItemEntry; +use serde::Deserialize; + +use crate::item::ItemStack; + +mod alternative; +mod item; + +#[derive(Deserialize, Clone)] +#[serde(tag = "type")] +pub enum LootPoolEntryTypes { + #[serde(rename = "minecraft:empty")] + Empty, + #[serde(rename = "minecraft:item")] + Item(ItemEntry), + #[serde(rename = "minecraft:loot_table")] + LootTable, + #[serde(rename = "minecraft:dynamic")] + Dynamic, + #[serde(rename = "minecraft:tag")] + Tag, + #[serde(rename = "minecraft:alternatives")] + Alternatives(AlternativeEntry), + #[serde(rename = "minecraft:sequence")] + Sequence, + #[serde(rename = "minecraft:group")] + Group, +} + +impl LootPoolEntryTypes { + pub fn get_items(&self) -> Vec { + match self { + LootPoolEntryTypes::Empty => todo!(), + LootPoolEntryTypes::Item(item_entry) => item_entry.get_items(), + LootPoolEntryTypes::LootTable => todo!(), + LootPoolEntryTypes::Dynamic => todo!(), + LootPoolEntryTypes::Tag => todo!(), + LootPoolEntryTypes::Alternatives(alternative) => alternative.get_items(), + LootPoolEntryTypes::Sequence => todo!(), + LootPoolEntryTypes::Group => todo!(), + } + } +} diff --git a/pumpkin-world/src/loot/mod.rs b/pumpkin-world/src/loot/mod.rs index ac1acc5e9..53fb7bc0c 100644 --- a/pumpkin-world/src/loot/mod.rs +++ b/pumpkin-world/src/loot/mod.rs @@ -1,25 +1,69 @@ +use condition::LootCondition; +use entry::LootPoolEntryTypes; use serde::Deserialize; +use crate::item::ItemStack; + +mod condition; +mod entry; + #[expect(dead_code)] #[derive(Deserialize, Clone)] pub struct LootTable { r#type: LootTableType, + random_sequence: Option, pools: Option>, } -#[expect(dead_code)] +impl LootTable { + pub fn get_loot(&self) -> Vec { + let mut items = vec![]; + if let Some(pools) = &self.pools { + for pool in pools { + items.extend_from_slice(&pool.get_loot()); + } + } + items + } +} + #[derive(Deserialize, Clone)] pub struct LootPool { entries: Vec, + rolls: f32, // TODO + bonus_rolls: f32, +} + +impl LootPool { + pub fn get_loot(&self) -> Vec { + let i = self.rolls.round() as i32 + self.bonus_rolls.floor() as i32; // TODO: mul by luck + let mut items = vec![]; + for _ in 0..i { + for entry in &self.entries { + if let Some(conditions) = &entry.conditions { + let mut conditions_met = true; + for condition in conditions { + if !condition.test() { + conditions_met = false; + break; + } + } + if !conditions_met { + continue; + } + } + items.extend_from_slice(&entry.content.get_items()); + } + } + items + } } -#[expect(dead_code)] #[derive(Deserialize, Clone)] pub struct LootPoolEntry { - // TODO - r#type: Option, - // TODO - name: Option, + #[serde(flatten)] + content: LootPoolEntryTypes, + conditions: Option>, } #[derive(Deserialize, Clone)] diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs index b11fb929e..10d3f1982 100644 --- a/pumpkin/src/block/mod.rs +++ b/pumpkin/src/block/mod.rs @@ -20,7 +20,10 @@ use properties::{ use pumpkin_data::entity::EntityType; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; -use pumpkin_world::block::registry::{Block, State}; +use pumpkin_world::{ + block::registry::{Block, State}, + item::ItemStack, +}; use rand::Rng; use crate::block::registry::BlockRegistry; @@ -57,7 +60,25 @@ pub async fn drop_loot( pos: &BlockPos, experience: bool, ) { - // TODO: Currently only the item block is dropped, We should drop the loop table + if let Some(table) = &block.loot_table { + let loot = table.get_loot(); + for item in loot { + drop_stack(server, world, pos, item).await; + } + } + + if experience { + if let Some(experience) = &block.experience { + let amount = experience.experience.get(); + // TODO: Silk touch gives no exp + if amount > 0 { + ExperienceOrbEntity::spawn(world, server, pos.to_f64(), amount as u32).await; + } + } + } +} + +async fn drop_stack(server: &Server, world: &Arc, pos: &BlockPos, stack: ItemStack) { let height = EntityType::ITEM.dimension[1] / 2.0; let pos = Vector3::new( f64::from(pos.0.x) + 0.5 + rand::thread_rng().gen_range(-0.25..0.25), @@ -66,19 +87,13 @@ pub async fn drop_loot( ); let entity = server.add_entity(pos, EntityType::ITEM, world); - let item_entity = Arc::new(ItemEntity::new(entity, block.item_id, 1)); + let item_entity = Arc::new(ItemEntity::new( + entity, + stack.item.id, + u32::from(stack.item_count), + )); world.spawn_entity(item_entity.clone()).await; item_entity.send_meta_packet().await; - - if experience { - if let Some(experience) = &block.experience { - let amount = experience.experience.get(); - // TODO: Silk touch gives no exp - if amount > 0 { - ExperienceOrbEntity::spawn(world, server, pos, amount as u32).await; - } - } - } } pub async fn calc_block_breaking(player: &Player, state: &State, block_name: &str) -> f32 {