fix: implement powder snow freeze effect and damage (#1390)

* fix: implement powder snow freeze effect and damage

- add frozen_ticks field to track freeze duration
- send DATA_FROZEN_TICKS metadata to clients for visual effect
- freeze ticks increase by 1/tick in powder snow, decrease by 2/tick outside
- deal 1 freeze damage every 40 ticks when fully frozen (140 ticks)
- add freeze immunity for snow golems, strays, polar bears, wither, wither skeletons

* fix: use freeze_immune_entity_types tag instead of hardcoded checks

* use has_tag method for freeze immunity check
This commit is contained in:
Purdze
2026-02-03 13:21:48 +00:00
committed by GitHub
parent 360e138eca
commit 2f29fcef8d
2 changed files with 67 additions and 2 deletions

View File

@@ -230,7 +230,7 @@ pub(crate) fn build() -> TokenStream {
impl Taggable for EntityType {
#[inline]
fn tag_key() -> RegistryKey {
RegistryKey::Block
RegistryKey::EntityType
}
#[inline]

View File

@@ -15,6 +15,7 @@ use pumpkin_data::block_properties::{EnumVariants, Integer0To15};
use pumpkin_data::dimension::Dimension;
use pumpkin_data::fluid::Fluid;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_data::{Block, BlockDirection};
use pumpkin_data::{
@@ -365,6 +366,9 @@ pub struct Entity {
pub damage_immunities: Vec<DamageType>,
pub fire_ticks: AtomicI32,
pub has_visual_fire: AtomicBool,
/// The number of ticks the entity has been frozen (in powder snow)
/// Max is 140 ticks (7 seconds). Increases by 1/tick in powder snow, decreases by 2/tick outside.
pub frozen_ticks: AtomicI32,
pub removal_reason: AtomicCell<Option<RemovalReason>>,
// The passengers that entity has
pub passengers: Mutex<Vec<Arc<dyn EntityBase>>>,
@@ -461,6 +465,7 @@ impl Entity {
data: AtomicI32::new(0),
fire_ticks: AtomicI32::new(-1),
has_visual_fire: AtomicBool::new(false),
frozen_ticks: AtomicI32::new(0),
removal_reason: AtomicCell::new(None),
passengers: Mutex::new(Vec::new()),
vehicle: Mutex::new(None),
@@ -1578,6 +1583,64 @@ impl Entity {
// TODO: defrost
}
/// Maximum freeze ticks (7 seconds at 20 tps)
pub const MAX_FROZEN_TICKS: i32 = 140;
/// Freeze damage is dealt every 40 ticks when fully frozen
const FREEZE_DAMAGE_INTERVAL: i32 = 40;
/// Check if the entity is currently in powder snow
pub async fn is_in_powder_snow(&self) -> bool {
let block_pos = self.block_pos.load();
self.world.load().get_block(&block_pos).await == &Block::POWDER_SNOW
}
/// Check if this entity type is immune to freezing
pub fn is_freeze_immune(&self) -> bool {
self.entity_type
.has_tag(&tag::EntityType::MINECRAFT_FREEZE_IMMUNE_ENTITY_TYPES)
}
/// Ticks the frozen state of the entity.
/// In powder snow: `frozen_ticks` increases by 1 (up to `MAX_FROZEN_TICKS`)
/// Outside powder snow: `frozen_ticks` decreases by 2 (down to 0)
/// When fully frozen, deals 1 damage every 40 ticks
pub async fn tick_frozen(&self, caller: &dyn EntityBase) {
// Freeze-immune entities don't accumulate freeze ticks
if self.is_freeze_immune() {
return;
}
let in_powder_snow = self.is_in_powder_snow().await;
let old_frozen_ticks = self.frozen_ticks.load(Ordering::Relaxed);
let new_frozen_ticks = if in_powder_snow {
// Increase frozen ticks when in powder snow
(old_frozen_ticks + 1).min(Self::MAX_FROZEN_TICKS)
} else {
// Decrease frozen ticks when not in powder snow (2x faster thaw rate)
(old_frozen_ticks - 2).max(0)
};
// Only update and send metadata if the value changed
if new_frozen_ticks != old_frozen_ticks {
self.frozen_ticks.store(new_frozen_ticks, Ordering::Relaxed);
self.send_meta_data(&[Metadata::new(
TrackedData::DATA_FROZEN_TICKS,
MetaDataType::Integer,
VarInt(new_frozen_ticks),
)])
.await;
}
// Deal freeze damage when fully frozen (every 40 ticks)
if new_frozen_ticks >= Self::MAX_FROZEN_TICKS
&& self.age.load(Ordering::Relaxed) % Self::FREEZE_DAMAGE_INTERVAL == 0
{
caller.damage(caller, 1.0, DamageType::FREEZE).await;
}
}
/// Sets the `Entity` yaw & pitch rotation
pub fn set_rotation(&self, yaw: f32, pitch: f32) {
// TODO
@@ -2102,7 +2165,9 @@ impl EntityBase for Entity {
}
self.set_on_fire(self.fire_ticks.load(Ordering::Relaxed) > 0)
.await;
// TODO: Tick
// Tick freeze state (powder snow)
self.tick_frozen(&*caller).await;
})
}