Skip to content

Commit

Permalink
Add Experience Orb and drop it when breaking Blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
Snowiiii committed Mar 1, 2025
1 parent 27f239a commit a60a3ba
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 30 deletions.
2 changes: 1 addition & 1 deletion pumpkin-protocol/src/client/play/set_experience.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use crate::VarInt;
#[packet(PLAY_SET_EXPERIENCE)]
pub struct CSetExperience {
progress: f32,
level: VarInt,
total_experience: VarInt,
level: VarInt,
}

impl CSetExperience {
Expand Down
2 changes: 2 additions & 0 deletions pumpkin-util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pumpkin-nbt = { path = "../pumpkin-nbt" }
serde.workspace = true
serde_json.workspace = true
bytes.workspace = true
rand = "0.9"


num-traits = "0.2"

Expand Down
63 changes: 63 additions & 0 deletions pumpkin-util/src/math/int_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use serde::Deserialize;

#[derive(Deserialize, Clone)]
#[serde(tag = "type")]
pub enum NormalInvProvider {
#[serde(rename = "minecraft:uniform")]
Uniform(UniformIntProvider),
// TODO: Add more...
}

#[derive(Deserialize, Clone)]
#[serde(untagged)]
pub enum InvProvider {
Object(NormalInvProvider),
Constant(i32),
}

impl InvProvider {
pub fn get_min(&self) -> i32 {
match self {
InvProvider::Object(inv_provider) => match inv_provider {
NormalInvProvider::Uniform(uniform) => uniform.get_min(),
},
InvProvider::Constant(i) => *i,
}
}

pub fn get(&self) -> i32 {
match self {
InvProvider::Object(inv_provider) => match inv_provider {
NormalInvProvider::Uniform(uniform) => uniform.get(),
},
InvProvider::Constant(i) => *i,
}
}

pub fn get_max(&self) -> i32 {
match self {
InvProvider::Object(inv_provider) => match inv_provider {
NormalInvProvider::Uniform(uniform) => uniform.get_max(),
},
InvProvider::Constant(i) => *i,
}
}
}

#[derive(Deserialize, Clone)]
pub struct UniformIntProvider {
min_inclusive: i32,
max_inclusive: i32,
}

impl UniformIntProvider {
pub fn get_min(&self) -> i32 {
self.min_inclusive
}
pub fn get(&self) -> i32 {
rand::random_range(self.min_inclusive..self.max_inclusive)
}
pub fn get_max(&self) -> i32 {
self.max_inclusive
}
}
1 change: 1 addition & 0 deletions pumpkin-util/src/math/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use num_traits::{One, PrimInt, Zero};

pub mod boundingbox;
pub mod experience;
pub mod int_provider;
pub mod position;
pub mod vector2;
pub mod vector3;
Expand Down
6 changes: 6 additions & 0 deletions pumpkin-world/src/block/registry.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::sync::LazyLock;

use pumpkin_util::math::int_provider::InvProvider;
use serde::Deserialize;

use crate::loot::LootTable;
Expand Down Expand Up @@ -118,6 +119,7 @@ pub struct Block {
pub id: u16,
pub item_id: u16,
pub hardness: f32,
pub experience: Option<Experience>,
pub blast_resistance: f32,
pub wall_variant_id: Option<u16>,
pub translation_key: String,
Expand All @@ -127,6 +129,10 @@ pub struct Block {
pub default_state_id: u16,
pub states: Vec<State>,
}
#[derive(Deserialize, Clone)]
pub struct Experience {
pub experience: InvProvider,
}
#[derive(Deserialize, Clone, Debug)]
pub struct Property {
pub name: String,
Expand Down
20 changes: 18 additions & 2 deletions pumpkin/src/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ use pumpkin_world::block::registry::{Block, State};
use pumpkin_world::item::ItemStack;
use rand::Rng;

use crate::block::blocks::jukebox::JukeboxBlock;
use crate::block::registry::BlockRegistry;
use crate::entity::item::ItemEntity;
use crate::server::Server;
use crate::world::World;
use crate::{block::blocks::crafting_table::CraftingTableBlock, entity::player::Player};
use crate::{block::blocks::jukebox::JukeboxBlock, entity::experience_orb::ExperienceOrbEntity};
use std::sync::Arc;

mod blocks;
Expand All @@ -52,7 +52,13 @@ pub fn default_registry() -> Arc<BlockRegistry> {
Arc::new(manager)
}

pub async fn drop_loot(server: &Server, world: &Arc<World>, block: &Block, pos: &BlockPos) {
pub async fn drop_loot(
server: &Server,
world: &Arc<World>,
block: &Block,
pos: &BlockPos,
experience: bool,
) {
// TODO: Currently only the item block is dropped, We should drop the loop table
let height = EntityType::ITEM.dimension[1] / 2.0;
let pos = Vector3::new(
Expand All @@ -68,6 +74,16 @@ pub async fn drop_loot(server: &Server, world: &Arc<World>, block: &Block, pos:
));
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 {
Expand Down
98 changes: 98 additions & 0 deletions pumpkin/src/entity/experience_orb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::sync::{Arc, atomic::AtomicU32};

use async_trait::async_trait;
use pumpkin_data::{damage::DamageType, entity::EntityType};
use pumpkin_util::math::vector3::Vector3;

use crate::{server::Server, world::World};

use super::{Entity, EntityBase, living::LivingEntity, player::Player};

pub struct ExperienceOrbEntity {
entity: Entity,
amount: u32,
orb_age: AtomicU32,
}

impl ExperienceOrbEntity {
pub fn new(entity: Entity, amount: u32) -> Self {
entity.yaw.store(rand::random::<f32>() * 360.0);
Self {
entity,
amount,
orb_age: AtomicU32::new(0),
}
}

pub async fn spawn(world: &Arc<World>, server: &Server, position: Vector3<f64>, amount: u32) {
let mut amount = amount;
while amount > 0 {
let i = Self::round_to_orb_size(amount);
amount -= i;
let entity = server.add_entity(position, EntityType::EXPERIENCE_ORB, world);
let orb = Arc::new(Self::new(entity, i));
world.spawn_entity(orb).await;
}
}

fn round_to_orb_size(value: u32) -> u32 {
if value >= 2477 {
2477
} else if value >= 1237 {
1237
} else if value >= 617 {
617
} else if value >= 307 {
307
} else if value >= 149 {
149
} else if value >= 73 {
73
} else if value >= 37 {
37
} else if value >= 17 {
17
} else if value >= 7 {
7
} else if value >= 3 {
3
} else {
1
}
}
}

#[async_trait]
impl EntityBase for ExperienceOrbEntity {
async fn tick(&self, _: &Server) {
let age = self
.orb_age
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if age >= 6000 {
self.entity.remove().await;
}
}

fn get_entity(&self) -> &Entity {
&self.entity
}

async fn on_player_collision(&self, player: Arc<Player>) {
let mut delay = player.experience_pick_up_delay.lock().await;
if *delay == 0 {
*delay = 2;
player.living_entity.pickup(&self.entity, 1).await;
player.add_experience_points(self.amount as i32).await;
// TODO: pickingCount for merging
self.entity.remove().await;
}
}

async fn damage(&self, _amount: f32, _damage_type: DamageType) -> bool {
false
}

fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
}
19 changes: 6 additions & 13 deletions pumpkin/src/entity/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::server::Server;
use async_trait::async_trait;
use pumpkin_data::damage::DamageType;
use pumpkin_protocol::{
client::play::{CTakeItemEntity, MetaDataType, Metadata},
client::play::{MetaDataType, Metadata},
codec::slot::Slot,
};
use pumpkin_world::item::ItemStack;
Expand All @@ -23,6 +23,7 @@ pub struct ItemEntity {

impl ItemEntity {
pub fn new(entity: Entity, stack: ItemStack) -> Self {
entity.yaw.store(rand::random::<f32>() * 360.0);
Self {
entity,
item: stack,
Expand Down Expand Up @@ -85,12 +86,8 @@ impl EntityBase for ItemEntity {
item.item_count = stack.item_count;

player
.client
.send_packet(&CTakeItemEntity::new(
self.entity.entity_id.into(),
player.entity_id().into(),
item.item_count.into(),
))
.living_entity
.pickup(&self.entity, u32::from(item.item_count))
.await;
self.entity.remove().await;
}
Expand All @@ -99,12 +96,8 @@ impl EntityBase for ItemEntity {
item.item_count = self.count.load(std::sync::atomic::Ordering::Relaxed);

player
.client
.send_packet(&CTakeItemEntity::new(
self.entity.entity_id.into(),
player.entity_id().into(),
item.item_count.into(),
))
.living_entity
.pickup(&self.entity, u32::from(item.item_count))
.await;
self.entity.remove().await;
}
Expand Down
17 changes: 16 additions & 1 deletion pumpkin/src/entity/living.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use pumpkin_config::ADVANCED_CONFIG;
use pumpkin_data::entity::{EffectType, EntityStatus};
use pumpkin_data::{damage::DamageType, sound::Sound};
use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::client::play::CHurtAnimation;
use pumpkin_protocol::client::play::{CHurtAnimation, CTakeItemEntity};
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::{
client::play::{CDamageEvent, CSetEquipment, EquipmentSlot, MetaDataType, Metadata},
Expand Down Expand Up @@ -70,6 +70,21 @@ impl LivingEntity {
.await;
}

/// Picks up and Item entity or XP Orb
pub async fn pickup(&self, item: &Entity, stack_amount: u32) {
// TODO: Only nearby
self.entity
.world
.read()
.await
.broadcast_packet_all(&CTakeItemEntity::new(
item.entity_id.into(),
self.entity.entity_id.into(),
stack_amount.into(),
))
.await;
}

pub fn set_pos(&self, position: Vector3<f64>) {
self.last_pos.store(self.entity.pos.load());
self.entity.set_pos(position);
Expand Down
1 change: 1 addition & 0 deletions pumpkin/src/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::world::World;

pub mod ai;
pub mod effect;
pub mod experience_orb;
pub mod hunger;
pub mod item;
pub mod living;
Expand Down
12 changes: 10 additions & 2 deletions pumpkin/src/entity/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ pub struct Player {
pub experience_progress: AtomicCell<f32>,
/// The player's total experience points
pub experience_points: AtomicI32,
pub experience_pick_up_delay: Mutex<u32>,
}

impl Player {
Expand Down Expand Up @@ -201,6 +202,7 @@ impl Player {
packet_sequence: AtomicI32::new(-1),
start_mining_time: AtomicI32::new(0),
carried_item: AtomicCell::new(None),
experience_pick_up_delay: Mutex::new(0),
teleport_id_count: AtomicI32::new(0),
mining: AtomicBool::new(false),
mining_pos: Mutex::new(BlockPos(Vector3::new(0, 0, 0))),
Expand Down Expand Up @@ -449,6 +451,12 @@ impl Player {
))
.await;
}
{
let mut xp = self.experience_pick_up_delay.lock().await;
if *xp > 0 {
*xp -= 1;
}
}

self.tick_counter.fetch_add(1, Ordering::Relaxed);

Expand Down Expand Up @@ -1050,8 +1058,8 @@ impl Player {
self.client
.send_packet(&CSetExperience::new(
progress.clamp(0.0, 1.0),
level.into(),
points.into(),
level.into(),
))
.await;
}
Expand Down Expand Up @@ -1140,7 +1148,7 @@ impl Player {
let total_exp = experience::points_to_level(current_level) + current_points;
let new_total_exp = total_exp + added_points;
let (new_level, new_points) = experience::total_to_level_and_points(new_total_exp);
let progress = experience::progress_in_level(new_level, new_points);
let progress = experience::progress_in_level(new_points, new_level);
self.set_experience(new_level, progress, new_points).await;
}
}
Expand Down
Loading

0 comments on commit a60a3ba

Please sign in to comment.