Implement fire/lava damage

Entities entering fire or lava now take damage and are extinguished upon entering water.
This commit is contained in:
Alexander Medvedev 2025-05-17 10:35:10 +02:00
parent 831275d155
commit ee32c60427
10 changed files with 122 additions and 11 deletions

View File

@ -1,7 +1,9 @@
use pumpkin_data::block_properties::HorizontalAxis;
use pumpkin_data::entity::EntityType;
use pumpkin_registry::DimensionType;
use rand::Rng;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use async_trait::async_trait;
use pumpkin_data::{Block, BlockState};
@ -13,6 +15,7 @@ use pumpkin_world::block::BlockDirection;
use pumpkin_world::chunk::TickPriority;
use crate::block::pumpkin_block::PumpkinBlock;
use crate::entity::EntityBase;
use crate::entity::player::Player;
use crate::server::Server;
use crate::world::World;
@ -65,6 +68,31 @@ impl PumpkinBlock for FireBlock {
.await;
}
async fn on_entity_collision(
&self,
_world: &Arc<World>,
entity: &dyn EntityBase,
_pos: BlockPos,
_block: Block,
_state: BlockState,
) {
let base_entity = entity.get_entity();
if !base_entity.entity_type.fire_immune {
let ticks = base_entity.fire_ticks.load(Ordering::Relaxed);
if ticks < 0 {
base_entity.fire_ticks.store(ticks + 1, Ordering::Relaxed);
} else if base_entity.entity_type == EntityType::PLAYER {
let rnd_ticks = rand::thread_rng().gen_range(1..3);
base_entity
.fire_ticks
.store(ticks + rnd_ticks, Ordering::Relaxed);
}
if base_entity.fire_ticks.load(Ordering::Relaxed) >= 0 {
base_entity.set_on_fire_for(8.0);
}
}
}
async fn get_state_for_neighbor_update(
&self,
world: &World,

View File

@ -6,9 +6,9 @@ use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::BlockStateId;
use crate::{block::pumpkin_fluid::PumpkinFluid, world::World};
use crate::{block::pumpkin_fluid::PumpkinFluid, entity::EntityBase, world::World};
use super::flowing_fluid::FlowingFluid;
use super::flowing::FlowingFluid;
#[pumpkin_block("minecraft:flowing_lava")]
pub struct FlowingLava;
@ -46,6 +46,13 @@ impl PumpkinFluid for FlowingLava {
.schedule_fluid_tick(fluid.id, *block_pos, LAVA_FLOW_SPEED)
.await;
}
async fn on_entity_collision(&self, entity: &dyn EntityBase) {
let base_entity = entity.get_entity();
if !base_entity.entity_type.fire_immune {
base_entity.set_on_fire_for(15.0);
}
}
}
#[async_trait]

View File

@ -1,3 +1,3 @@
pub(crate) mod flowing_fluid;
pub(crate) mod flowing;
pub(crate) mod lava;
pub(crate) mod water;

View File

@ -6,9 +6,9 @@ use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::BlockStateId;
use crate::{block::pumpkin_fluid::PumpkinFluid, world::World};
use crate::{block::pumpkin_fluid::PumpkinFluid, entity::EntityBase, world::World};
use super::flowing_fluid::FlowingFluid;
use super::flowing::FlowingFluid;
#[pumpkin_block("minecraft:flowing_water")]
pub struct FlowingWater;
@ -46,6 +46,10 @@ impl PumpkinFluid for FlowingWater {
.schedule_fluid_tick(fluid.id, *block_pos, WATER_FLOW_SPEED)
.await;
}
async fn on_entity_collision(&self, entity: &dyn EntityBase) {
entity.get_entity().extinguish();
}
}
#[async_trait]

View File

@ -42,8 +42,8 @@ use blocks::walls::WallBlock;
use blocks::{
chest::ChestBlock, furnace::FurnaceBlock, redstone::lever::LeverBlock, tnt::TNTBlock,
};
use fluids::lava::FlowingLava;
use fluids::water::FlowingWater;
use fluid::lava::FlowingLava;
use fluid::water::FlowingWater;
use loot::LootTableExt;
use pumpkin_data::block_properties::Integer0To15;
use pumpkin_data::entity::EntityType;
@ -63,7 +63,7 @@ use crate::{block::blocks::jukebox::JukeboxBlock, entity::experience_orb::Experi
use std::sync::Arc;
pub(crate) mod blocks;
mod fluids;
mod fluid;
mod loot;
pub mod pumpkin_block;
pub mod pumpkin_fluid;

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::entity::player::Player;
use crate::entity::{EntityBase, player::Player};
use async_trait::async_trait;
use pumpkin_data::{fluid::Fluid, item::Item};
use pumpkin_protocol::server::play::SUseItemOn;
@ -78,6 +78,8 @@ pub trait PumpkinFluid: Send + Sync {
) {
}
async fn on_entity_collision(&self, _entity: &dyn EntityBase) {}
async fn on_scheduled_tick(&self, _world: &Arc<World>, _fluid: &Fluid, _block_pos: &BlockPos) {}
async fn create_legacy_block(&self, _world: &World, _block_pos: &BlockPos) {}

View File

@ -72,6 +72,13 @@ impl BlockRegistry {
}
}
pub async fn on_entity_collision_fluid(&self, fluid: &Fluid, entity: &dyn EntityBase) {
let pumpkin_fluid = self.get_pumpkin_fluid(fluid);
if let Some(pumpkin_fluid) = pumpkin_fluid {
pumpkin_fluid.on_entity_collision(entity).await;
}
}
pub async fn on_use(
&self,
block: &Block,

View File

@ -138,6 +138,8 @@ pub struct Entity {
pub invulnerable: AtomicBool,
/// List of damage types this entity is immune to
pub damage_immunities: Vec<DamageType>,
pub fire_ticks: AtomicI32,
pub has_visual_fire: AtomicBool,
pub portal_cooldown: AtomicU32,
@ -189,6 +191,8 @@ impl Entity {
bounding_box_size: AtomicCell::new(bounding_box_size),
invulnerable: AtomicBool::new(invulnerable),
damage_immunities: Vec::new(),
fire_ticks: AtomicI32::new(-1),
has_visual_fire: AtomicBool::new(false),
portal_cooldown: AtomicU32::new(0),
portal_manager: Mutex::new(None),
}
@ -353,6 +357,22 @@ impl Entity {
}
}
/// Extinguishes this entity.
pub fn extinguish(&self) {
self.fire_ticks.store(0, Ordering::Relaxed);
}
pub fn set_on_fire_for(&self, seconds: f32) {
self.set_on_fire_for_ticks((seconds * 20.0).floor() as u32);
}
pub fn set_on_fire_for_ticks(&self, ticks: u32) {
if self.fire_ticks.load(Ordering::Relaxed) < ticks as i32 {
self.fire_ticks.store(ticks as i32, Ordering::Relaxed);
}
// TODO: defrost
}
/// Sets the `Entity` yaw & pitch rotation
pub fn set_rotation(&self, yaw: f32, pitch: f32) {
// TODO
@ -424,6 +444,13 @@ impl Entity {
}
}
pub async fn set_on_fire(&self, on_fire: bool) {
if self.has_visual_fire.load(Ordering::Relaxed) != on_fire {
self.has_visual_fire.store(on_fire, Ordering::Relaxed);
self.set_flag(Flag::OnFire, on_fire).await;
}
}
pub fn get_horizontal_facing(&self) -> HorizontalFacing {
let adjusted_yaw = (self.yaw.load() % 360.0 + 360.0) % 360.0; // Normalize yaw to [0, 360)
@ -572,6 +599,12 @@ impl Entity {
.on_entity_collision(block, &world, entity, pos, state)
.await;
}
if let Ok(fluid) = world.get_fluid(&pos).await {
world
.block_registry
.on_entity_collision_fluid(&fluid, entity)
.await;
}
}
}
}
@ -584,8 +617,25 @@ impl EntityBase for Entity {
false
}
async fn tick(&self, _caller: &dyn EntityBase, _: &Server) {
async fn tick(&self, caller: &dyn EntityBase, _: &Server) {
self.tick_portal().await;
let fire_ticks = self.fire_ticks.load(Ordering::Relaxed);
if fire_ticks > 0 {
if self.entity_type.fire_immune {
self.fire_ticks.store(fire_ticks - 4, Ordering::Relaxed);
if self.fire_ticks.load(Ordering::Relaxed) < 0 {
self.extinguish();
}
} else {
if fire_ticks % 20 == 0 {
caller.damage(1.0, DamageType::ON_FIRE).await;
}
self.fire_ticks.store(fire_ticks - 1, Ordering::Relaxed);
}
}
self.set_on_fire(self.fire_ticks.load(Ordering::Relaxed) > 0)
.await;
// TODO: Tick
}
@ -619,7 +669,13 @@ impl NBTStorage for Entity {
"Rotation",
NbtTag::List(vec![self.yaw.load().into(), self.pitch.load().into()].into_boxed_slice()),
);
nbt.put_short("Fire", self.fire_ticks.load(Relaxed) as i16);
nbt.put_bool("OnGround", self.on_ground.load(Relaxed));
nbt.put_bool("Invulnerable", self.invulnerable.load(Relaxed));
nbt.put_int("PortalCooldown", self.portal_cooldown.load(Relaxed) as i32);
if self.has_visual_fire.load(Relaxed) {
nbt.put_bool("HasVisualFire", true);
}
// todo more...
}
@ -640,8 +696,16 @@ impl NBTStorage for Entity {
let pitch = rotation[1].extract_float().unwrap_or(0.0);
self.set_rotation(yaw, pitch);
self.head_yaw.store(yaw);
self.fire_ticks
.store(i32::from(nbt.get_short("Fire").unwrap_or(0)), Relaxed);
self.on_ground
.store(nbt.get_bool("OnGround").unwrap_or(false), Relaxed);
self.invulnerable
.store(nbt.get_bool("Invulnerable").unwrap_or(false), Relaxed);
self.portal_cooldown
.store(nbt.get_int("PortalCooldown").unwrap_or(0) as u32, Relaxed);
self.has_visual_fire
.store(nbt.get_bool("HasVisualFire").unwrap_or(false), Relaxed);
// todo more...
}
}

View File

@ -277,7 +277,6 @@ impl Server {
}
} else {
// Player data exists but doesn't have a "Dimension" key.
log::warn!("Player data missing 'Dimension' key, defaulting to Overworld.");
let default_world_guard = self.worlds.read().await;
let default_world = default_world_guard
.first()