feat(bedrock): death animation

This commit is contained in:
Alexander Medvedev
2026-08-09 17:29:54 +02:00
parent 33e11088b2
commit 92658f5853
14 changed files with 126 additions and 40 deletions

View File

@@ -15,7 +15,7 @@ pub struct SActorEvent {
pub fire_at_position: Option<Vector3<f32>>,
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ActorEventType {
Jump = 1,

View File

@@ -13,6 +13,8 @@ use crate::{
world::World,
};
use crate::net::ClientPlatform;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttackType {
Knockback,
@@ -38,6 +40,7 @@ impl AttackType {
}
let sword = held_item.is_sword();
let is_bedrock = matches!(player.client.as_ref(), ClientPlatform::Bedrock(_));
let is_strong = attack_cooldown_progress > 0.9;
if sprinting && is_strong {
@@ -48,7 +51,7 @@ impl AttackType {
return Self::Critical;
}
if sword && is_strong {
if sword && is_strong && !is_bedrock {
return Self::Sweeping;
}

View File

@@ -411,7 +411,7 @@ impl EntityBase for ArmorStandEntity {
let time = world.level_time.lock().await.query_gametime();
if time - self.last_hit_time.load(Ordering::Relaxed) > 5 && !always_kills {
world.send_entity_status(entity, EntityStatus::ArmorstandWobble);
world.send_entity_status(entity, EntityStatus::ArmorstandWobble, None);
world.play_sound(
Sound::EntityArmorStandHit,
SoundCategory::Neutral,

View File

@@ -279,7 +279,23 @@ impl LivingEntity {
&& item.get_data_component::<BlocksAttacksImpl>().is_some()
{
let use_time = self.item_use_time.load(Ordering::Relaxed);
return item.get_max_use_time() - use_time >= 5;
let required_time = if let Some(dyn_self) = self
.entity
.world
.load()
.get_entity_by_id(self.entity.entity_id)
&& let Some(player) = dyn_self
.cast_any()
.downcast_ref::<crate::entity::player::Player>()
&& matches!(
player.client.as_ref(),
crate::net::ClientPlatform::Bedrock(_)
) {
0
} else {
5
};
return item.get_max_use_time() - use_time >= required_time;
}
false
}
@@ -1355,7 +1371,11 @@ impl LivingEntity {
self.update_death_stats(&*dyn_self, cause).await;
// Plays the death sound
world.send_entity_status(&self.entity, EntityStatus::Death);
world.send_entity_status(
&self.entity,
EntityStatus::Death,
Some(ActorEventType::Death),
);
let looting_level;
let tool = if let Some(cause_ent) = cause {
if let Some(player) = cause_ent
@@ -1747,10 +1767,11 @@ impl LivingEntity {
.insert(slot, stack);
}
self.set_health(1.0);
self.entity
.world
.load()
.send_entity_status(&self.entity, EntityStatus::ProtectedFromDeath);
self.entity.world.load().send_entity_status(
&self.entity,
EntityStatus::ProtectedFromDeath,
Some(ActorEventType::InstantDeath),
);
// Set Absorption, Regeneration, and Fire Resistance effects
self.add_effect(Effect {
@@ -1828,8 +1849,11 @@ impl LivingEntity {
if slot_result != pumpkin_data::item_stack::DamageResult::Untouched {
if slot_result == pumpkin_data::item_stack::DamageResult::Broken {
let world = self.entity.world.load();
world
.send_entity_status(&self.entity, super::equipment_break_status(&slot));
world.send_entity_status(
&self.entity,
super::equipment_break_status(&slot),
None,
);
}
equipment_updates.push((slot.clone(), stack.clone()));
if let Some(player) = caller.get_player() {
@@ -2358,6 +2382,7 @@ impl EntityBase for LivingEntity {
world.send_entity_status(
&self.entity,
crate::entity::equipment_break_status(&slot),
None,
);
*stack = ItemStack::EMPTY.clone();
let broken_stack = stack.clone();
@@ -2759,10 +2784,11 @@ impl EntityBase for LivingEntity {
// Only send death particles once (on the exact tick death_time reaches 20)
// and then remove the entity, preventing entity_event spam.
if time == 20 && !self.entity.removed.swap(true, Ordering::Relaxed) {
self.entity
.world
.load()
.send_entity_status(&self.entity, EntityStatus::Death);
self.entity.world.load().send_entity_status(
&self.entity,
EntityStatus::Death,
Some(ActorEventType::Death),
);
self.entity.remove().await;
}
}

View File

@@ -5,6 +5,7 @@ use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use crate::entity::{EntityBaseFuture, mob::Mob, player::Player};
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_util::math::vector3::Vector3;
pub trait Animal: Mob {
@@ -61,6 +62,7 @@ pub trait Animal: Mob {
world.send_entity_status(
entity,
pumpkin_data::entity::EntityStatus::InLoveHearts,
Some(ActorEventType::InLoveHearts),
);
world.spawn_particle(

View File

@@ -18,6 +18,7 @@ use pumpkin_inventory::screen_handler::{
BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler,
};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
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};
@@ -282,10 +283,11 @@ impl VillagerEntity {
pub fn set_unhappy(&self) {
let entity = self.get_entity();
entity
.world
.load()
.send_entity_status(entity, pumpkin_data::entity::EntityStatus::VillagerAngry);
entity.world.load().send_entity_status(
entity,
pumpkin_data::entity::EntityStatus::VillagerAngry,
Some(ActorEventType::VillagerAngry),
);
entity.play_sound(pumpkin_data::sound::Sound::EntityVillagerNo);
}
@@ -379,6 +381,7 @@ impl ScreenHandlerFactory for VillagerEntity {
entity.world.load().send_entity_status(
entity,
pumpkin_data::entity::EntityStatus::VillagerHappy,
Some(ActorEventType::VillagerHappy),
);
entity.play_sound(
pumpkin_data::sound::Sound::EntityVillagerCelebrate,

View File

@@ -59,8 +59,10 @@ use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::IdOr;
use pumpkin_protocol::SoundEvent;
use pumpkin_protocol::bedrock::client::container_open::CContainerOpen;
use pumpkin_protocol::bedrock::server::actor_event::{ActorEventType, SActorEvent};
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::codec::var_long::VarLong;
use pumpkin_protocol::codec::var_ulong::VarULong;
use pumpkin_protocol::java::client::play::{
Animation, CActionBar, CAwardStats, CChangeDifficulty, CCloseContainer, CCombatDeath,
CCustomPayload, CDisguisedChatMessage, CEntityAnimation, CEntityPositionSync, CGameEvent,
@@ -1085,11 +1087,12 @@ impl Player {
let attack_speed = base_attack_speed + add_speed;
let attack_cooldown_progress = self.get_attack_cooldown_progress(
f64::from(server.basic_config.tps),
0.5,
attack_speed,
);
let is_bedrock = matches!(self.client.as_ref(), ClientPlatform::Bedrock(_));
let attack_cooldown_progress = if is_bedrock {
1.0
} else {
self.get_attack_cooldown_progress(f64::from(server.basic_config.tps), 0.5, attack_speed)
};
self.last_attacked_ticks.store(0, Ordering::Relaxed);
// Only reduce attack damage if in cooldown
@@ -1349,6 +1352,7 @@ impl Player {
self.world().send_entity_status(
&self.living_entity.entity,
super::equipment_break_status(slot),
None,
);
}
@@ -2589,7 +2593,7 @@ impl Player {
PermissionLvl::Four => EntityStatus::PermissionLevelOwners,
};
self.world()
.send_entity_status(&self.living_entity.entity, status);
.send_entity_status(&self.living_entity.entity, status, None);
}
/// Sets the player's difficulty level.
@@ -3250,8 +3254,17 @@ impl Player {
self.breath_manager.reset(self);
self.client
.send_packet_now(&CCombatDeath::new(self.entity_id().into(), &death_msg))
.send_packet_now_editioned(
&CCombatDeath::new(self.entity_id().into(), &death_msg),
&SActorEvent {
entity_runtime_id: VarULong(self.entity_id() as u64),
event_type: ActorEventType::Death,
event_data: VarInt(0),
fire_at_position: None,
},
)
.await;
self.send_health().await;
}
pub async fn set_gamemode(self: &Arc<Self>, gamemode: GameMode) -> bool {

View File

@@ -14,6 +14,7 @@ use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::math::vector3::Vector3;
@@ -114,7 +115,11 @@ impl EntityBase for EggEntity {
let spawn_pos = hit_pos.add(&normal.multiply(0.5, 0.5, 0.5));
// Play egg break particles
world.send_entity_status(self.get_entity(), EntityStatus::Death);
world.send_entity_status(
self.get_entity(),
EntityStatus::Death,
Some(ActorEventType::Death),
);
// Decide spawn count per probabilities:
// r == 0 -> spawn 4 (1/256)

View File

@@ -13,6 +13,7 @@ use pumpkin_data::damage::DamageType;
use pumpkin_data::entity::{EntityPose, EntityStatus};
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_util::math::vector3::Vector3;
const GRAVITY: f64 = 0.03;
@@ -177,7 +178,7 @@ impl EntityBase for EnderPearlEntity {
.await;
}
world.send_entity_status(entity, EntityStatus::Death);
world.send_entity_status(entity, EntityStatus::Death, Some(ActorEventType::Death));
})
}
}

View File

@@ -4,6 +4,7 @@ use crate::{
world::World,
};
use pumpkin_data::{entity::EntityStatus, meta_data_type::MetaDataType, tracked_data::TrackedData};
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_protocol::{codec::optional_int::OptionalInt, java::client::play::Metadata};
use pumpkin_util::{
math::vector3::Vector3,
@@ -82,7 +83,11 @@ impl FireworkRocketEntity {
pub async fn explode_and_remove(&self, world: &World) {
let entity = self.get_entity();
world.send_entity_status(entity, EntityStatus::FireworksExplode);
world.send_entity_status(
entity,
EntityStatus::FireworksExplode,
Some(ActorEventType::FireworksExplode),
);
// TODO: Explode/colors

View File

@@ -8,6 +8,7 @@ use crate::{
};
use pumpkin_data::entity::EntityStatus;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_protocol::java::client::play::CWorldEvent;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector2::{Vector2, to_chunk_pos};
@@ -116,7 +117,11 @@ impl EntityBase for LingeringPotionEntity {
extinguish_fire_if_water_potion(&world, hit_pos, &stack).await;
// Play impact particles
world.send_entity_status(self.get_entity(), EntityStatus::Death);
world.send_entity_status(
self.get_entity(),
EntityStatus::Death,
Some(ActorEventType::Death),
);
// Read stored item stack and compute potion effects
let stack = self.item_stack.read().await.clone();

View File

@@ -8,6 +8,7 @@ use crate::{
};
use pumpkin_data::damage::DamageType;
use pumpkin_data::entity::{EntityStatus, EntityType};
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_util::math::vector3::Vector3;
const GRAVITY: f64 = 0.03;
@@ -72,7 +73,11 @@ impl EntityBase for SnowballEntity {
let world = self.get_entity().world.load();
// Always send particle status regardless of what was hit
world.send_entity_status(self.get_entity(), EntityStatus::Death);
world.send_entity_status(
self.get_entity(),
EntityStatus::Death,
Some(ActorEventType::Death),
);
// Handle entity-specific damage
if let ProjectileHit::Entity { ref entity, .. } = hit {

View File

@@ -2411,9 +2411,11 @@ impl JavaClient {
} else {
&EquipmentSlot::OFF_HAND
};
player
.world()
.send_entity_status(player.get_entity(), equipment_break_status(slot));
player.world().send_entity_status(
player.get_entity(),
equipment_break_status(slot),
None,
);
}
if !after.are_equal(&before) {

View File

@@ -98,7 +98,10 @@ use pumpkin_protocol::{
start_game::{Experiments, GamePublishSetting, LevelSettings},
update_attributes::{Attribute, CUpdateAttributes},
},
server::text::SText,
server::{
actor_event::{ActorEventType, SActorEvent},
text::SText,
},
},
codec::{var_int::VarInt, var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong},
java::{
@@ -406,12 +409,25 @@ impl World {
}
/// Sends an entity status update to all players tracking the specified entity.
pub fn send_entity_status(&self, entity: &Entity, status: EntityStatus) {
pub fn send_entity_status(
&self,
entity: &Entity,
java_status: EntityStatus,
bedrock_status: Option<ActorEventType>,
) {
let chunk_pos = entity.chunk_pos.load();
self.broadcast_to_chunk(
chunk_pos,
&CEntityStatus::new(entity.entity_id, status as i8),
);
let je_packet = CEntityStatus::new(entity.entity_id, java_status as i8);
if let Some(be_event) = bedrock_status {
let be_packet = SActorEvent {
entity_runtime_id: VarULong(entity.entity_id as u64),
event_type: be_event,
event_data: VarInt(0),
fire_at_position: None,
};
self.broadcast_to_chunk_editioned_sync(chunk_pos, &je_packet, &be_packet);
} else {
self.broadcast_to_chunk(chunk_pos, &je_packet);
}
}
pub fn send_remove_mob_effect(&self, entity: &Entity, effect_type: &'static StatusEffect) {