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>>, pub fire_at_position: Option<Vector3<f32>>,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)] #[repr(u8)]
pub enum ActorEventType { pub enum ActorEventType {
Jump = 1, Jump = 1,

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ use pumpkin_inventory::screen_handler::{
BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler, BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler,
}; };
use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::{CMerchantOffers, Metadata}; use pumpkin_protocol::java::client::play::{CMerchantOffers, Metadata};
use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}; use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3};
@ -282,10 +283,11 @@ impl VillagerEntity {
pub fn set_unhappy(&self) { pub fn set_unhappy(&self) {
let entity = self.get_entity(); let entity = self.get_entity();
entity entity.world.load().send_entity_status(
.world entity,
.load() pumpkin_data::entity::EntityStatus::VillagerAngry,
.send_entity_status(entity, pumpkin_data::entity::EntityStatus::VillagerAngry); Some(ActorEventType::VillagerAngry),
);
entity.play_sound(pumpkin_data::sound::Sound::EntityVillagerNo); entity.play_sound(pumpkin_data::sound::Sound::EntityVillagerNo);
} }
@ -379,6 +381,7 @@ impl ScreenHandlerFactory for VillagerEntity {
entity.world.load().send_entity_status( entity.world.load().send_entity_status(
entity, entity,
pumpkin_data::entity::EntityStatus::VillagerHappy, pumpkin_data::entity::EntityStatus::VillagerHappy,
Some(ActorEventType::VillagerHappy),
); );
entity.play_sound( entity.play_sound(
pumpkin_data::sound::Sound::EntityVillagerCelebrate, pumpkin_data::sound::Sound::EntityVillagerCelebrate,

View File

@ -59,8 +59,10 @@ use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::IdOr; use pumpkin_protocol::IdOr;
use pumpkin_protocol::SoundEvent; use pumpkin_protocol::SoundEvent;
use pumpkin_protocol::bedrock::client::container_open::CContainerOpen; 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_int::VarInt;
use pumpkin_protocol::codec::var_long::VarLong; use pumpkin_protocol::codec::var_long::VarLong;
use pumpkin_protocol::codec::var_ulong::VarULong;
use pumpkin_protocol::java::client::play::{ use pumpkin_protocol::java::client::play::{
Animation, CActionBar, CAwardStats, CChangeDifficulty, CCloseContainer, CCombatDeath, Animation, CActionBar, CAwardStats, CChangeDifficulty, CCloseContainer, CCombatDeath,
CCustomPayload, CDisguisedChatMessage, CEntityAnimation, CEntityPositionSync, CGameEvent, CCustomPayload, CDisguisedChatMessage, CEntityAnimation, CEntityPositionSync, CGameEvent,
@ -1085,11 +1087,12 @@ impl Player {
let attack_speed = base_attack_speed + add_speed; let attack_speed = base_attack_speed + add_speed;
let attack_cooldown_progress = self.get_attack_cooldown_progress( let is_bedrock = matches!(self.client.as_ref(), ClientPlatform::Bedrock(_));
f64::from(server.basic_config.tps), let attack_cooldown_progress = if is_bedrock {
0.5, 1.0
attack_speed, } else {
); self.get_attack_cooldown_progress(f64::from(server.basic_config.tps), 0.5, attack_speed)
};
self.last_attacked_ticks.store(0, Ordering::Relaxed); self.last_attacked_ticks.store(0, Ordering::Relaxed);
// Only reduce attack damage if in cooldown // Only reduce attack damage if in cooldown
@ -1349,6 +1352,7 @@ impl Player {
self.world().send_entity_status( self.world().send_entity_status(
&self.living_entity.entity, &self.living_entity.entity,
super::equipment_break_status(slot), super::equipment_break_status(slot),
None,
); );
} }
@ -2589,7 +2593,7 @@ impl Player {
PermissionLvl::Four => EntityStatus::PermissionLevelOwners, PermissionLvl::Four => EntityStatus::PermissionLevelOwners,
}; };
self.world() 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. /// Sets the player's difficulty level.
@ -3250,8 +3254,17 @@ impl Player {
self.breath_manager.reset(self); self.breath_manager.reset(self);
self.client 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; .await;
self.send_health().await;
} }
pub async fn set_gamemode(self: &Arc<Self>, gamemode: GameMode) -> bool { 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::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tracked_data::TrackedData; 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::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_protocol::java::client::play::Metadata; use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::math::vector3::Vector3; 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)); let spawn_pos = hit_pos.add(&normal.multiply(0.5, 0.5, 0.5));
// Play egg break particles // 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: // Decide spawn count per probabilities:
// r == 0 -> spawn 4 (1/256) // 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::entity::{EntityPose, EntityStatus};
use pumpkin_data::particle::Particle; use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
const GRAVITY: f64 = 0.03; const GRAVITY: f64 = 0.03;
@ -177,7 +178,7 @@ impl EntityBase for EnderPearlEntity {
.await; .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, world::World,
}; };
use pumpkin_data::{entity::EntityStatus, meta_data_type::MetaDataType, tracked_data::TrackedData}; 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_protocol::{codec::optional_int::OptionalInt, java::client::play::Metadata};
use pumpkin_util::{ use pumpkin_util::{
math::vector3::Vector3, math::vector3::Vector3,
@ -82,7 +83,11 @@ impl FireworkRocketEntity {
pub async fn explode_and_remove(&self, world: &World) { pub async fn explode_and_remove(&self, world: &World) {
let entity = self.get_entity(); 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 // TODO: Explode/colors

View File

@ -8,6 +8,7 @@ use crate::{
}; };
use pumpkin_data::entity::EntityStatus; use pumpkin_data::entity::EntityStatus;
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_protocol::java::client::play::CWorldEvent; use pumpkin_protocol::java::client::play::CWorldEvent;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector2::{Vector2, to_chunk_pos}; 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; extinguish_fire_if_water_potion(&world, hit_pos, &stack).await;
// Play impact particles // 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 // Read stored item stack and compute potion effects
let stack = self.item_stack.read().await.clone(); let stack = self.item_stack.read().await.clone();

View File

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

View File

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

View File

@ -98,7 +98,10 @@ use pumpkin_protocol::{
start_game::{Experiments, GamePublishSetting, LevelSettings}, start_game::{Experiments, GamePublishSetting, LevelSettings},
update_attributes::{Attribute, CUpdateAttributes}, 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}, codec::{var_int::VarInt, var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong},
java::{ java::{
@ -406,12 +409,25 @@ impl World {
} }
/// Sends an entity status update to all players tracking the specified entity. /// 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(); let chunk_pos = entity.chunk_pos.load();
self.broadcast_to_chunk( let je_packet = CEntityStatus::new(entity.entity_id, java_status as i8);
chunk_pos, if let Some(be_event) = bedrock_status {
&CEntityStatus::new(entity.entity_id, status as i8), 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) { pub fn send_remove_mob_effect(&self, entity: &Entity, effect_type: &'static StatusEffect) {