Add Experience Orb and drop it when breaking Blocks

This commit is contained in:
Alexander Medvedev
2025-03-01 16:58:02 +01:00
parent 27f239a612
commit a60a3ba243
13 changed files with 225 additions and 30 deletions

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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;
@@ -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,
@@ -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,

View File

@@ -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;
@@ -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(
@@ -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 {

View File

@@ -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
}
}

View File

@@ -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;
@@ -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,
@@ -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;
}
@@ -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;
}

View File

@@ -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},
@@ -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);

View File

@@ -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;

View File

@@ -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 {
@@ -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))),
@@ -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);
@@ -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;
}
@@ -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;
}
}

View File

@@ -82,7 +82,7 @@ impl Explosion {
let block = world.get_block(&pos).await.unwrap();
let pumpkin_block = server.block_registry.get_pumpkin_block(block);
if pumpkin_block.is_none_or(|s| s.should_drop_items_on_explosion()) {
drop_loot(server, world, block, &pos).await;
drop_loot(server, world, block, &pos, false).await;
}
if let Some(pumpkin_block) = pumpkin_block {
pumpkin_block.explode(block, world, pos, server).await;

View File

@@ -1003,15 +1003,7 @@ impl World {
}
}
/// Adds a living entity to the world.
///
/// This function takes a living entity's UUID and an `Arc<LivingEntity>` reference.
/// It inserts the living entity into the world's `current_living_entities` map using the UUID as the key.
///
/// # Arguments
///
/// * `uuid`: The unique UUID of the living entity to add.
/// * `living_entity`: A `Arc<LivingEntity>` reference to the living entity object.
/// Adds a entity to the world.
pub async fn spawn_entity(&self, entity: Arc<dyn EntityBase>) {
let base_entity = entity.get_entity();
self.broadcast_packet_all(&base_entity.create_spawn_packet())
@@ -1120,7 +1112,7 @@ impl World {
);
if drop {
block::drop_loot(server, self, block, position).await;
block::drop_loot(server, self, block, position, true).await;
}
match cause {