chore: make explosion make match more vanilla

This commit is contained in:
Alexander Medvedev 2026-08-20 09:22:29 +02:00
parent 0844e92911
commit dd8ba62499
11 changed files with 462 additions and 95 deletions

View File

@ -244,7 +244,11 @@ impl BlockBehaviour for BedBlock {
.await; .await;
args.world args.world
.explode(bed_head_pos.to_centered_f64(), 5.0) .explode(
bed_head_pos.to_centered_f64(),
5.0,
crate::world::ExplosionInteraction::Block,
)
.await; .await;
return BlockActionResult::SuccessServer; return BlockActionResult::SuccessServer;

View File

@ -62,7 +62,11 @@ impl BlockBehaviour for RespawnAnchorBlock {
.break_block(args.position, None, BlockFlags::SKIP_DROPS) .break_block(args.position, None, BlockFlags::SKIP_DROPS)
.await; .await;
args.world args.world
.explode(args.position.to_centered_f64(), 5.0) .explode(
args.position.to_centered_f64(),
5.0,
crate::world::ExplosionInteraction::Block,
)
.await; .await;
return BlockActionResult::SuccessServer; return BlockActionResult::SuccessServer;
} }

View File

@ -56,7 +56,11 @@ impl EntityBase for EndCrystalEntity {
self.entity self.entity
.world .world
.load() .load()
.explode(self.entity.pos.load(), 6.0) .explode(
self.entity.pos.load(),
6.0,
crate::world::ExplosionInteraction::Block,
)
.await; .await;
} }

View File

@ -115,7 +115,13 @@ impl CreeperEntity {
.store(true, Ordering::Relaxed); .store(true, Ordering::Relaxed);
let world = entity.world.load(); let world = entity.world.load();
let pos = entity.pos.load(); let pos = entity.pos.load();
world.explode(pos, radius * multiplier).await; world
.explode(
pos,
radius * multiplier,
crate::world::ExplosionInteraction::Mob,
)
.await;
// TODO: spawn area effect cloud with potion effects // TODO: spawn area effect cloud with potion effects
entity.remove().await; entity.remove().await;
} }

View File

@ -272,7 +272,13 @@ impl EntityBase for FireballEntity {
} }
let hit_pos = hit.hit_pos(); let hit_pos = hit.hit_pos();
world.explode(hit_pos, self.get_explosion_power()).await; world
.explode(
hit_pos,
self.get_explosion_power(),
crate::world::ExplosionInteraction::Mob,
)
.await;
}) })
} }
} }

View File

@ -33,6 +33,31 @@ pub struct WindChargeEntity {
thrown_item_entity: ThrownItemEntity, thrown_item_entity: ThrownItemEntity,
} }
use crate::world::SimpleExplosionDamageCalculator;
use pumpkin_data::tag;
use std::sync::LazyLock;
pub static WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR: LazyLock<Arc<SimpleExplosionDamageCalculator>> =
LazyLock::new(|| {
Arc::new(SimpleExplosionDamageCalculator::new(
true,
false,
Some(1.22),
Some(&tag::Block::MINECRAFT_BLOCKS_WIND_CHARGE_EXPLOSIONS),
))
});
pub static BREEZE_WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR: LazyLock<
Arc<SimpleExplosionDamageCalculator>,
> = LazyLock::new(|| {
Arc::new(SimpleExplosionDamageCalculator::new(
true,
false,
None,
Some(&tag::Block::MINECRAFT_BLOCKS_WIND_CHARGE_EXPLOSIONS),
))
});
impl WindChargeEntity { impl WindChargeEntity {
#[must_use] #[must_use]
pub const fn new_normal(thrown_item_entity: ThrownItemEntity) -> Self { pub const fn new_normal(thrown_item_entity: ThrownItemEntity) -> Self {
@ -64,10 +89,19 @@ impl WindChargeEntity {
} }
pub async fn create_explosion(&self, position: Vector3<f64>) { pub async fn create_explosion(&self, position: Vector3<f64>) {
let calculator = match self.kind {
WindChargeKind::Normal { .. } => WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR.clone(),
WindChargeKind::Breeze => BREEZE_WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR.clone(),
};
self.get_entity() self.get_entity()
.world .world
.load() .load()
.explode(position, EXPLOSION_POWER) .explode_with_calculator(
position,
EXPLOSION_POWER,
crate::world::ExplosionInteraction::Trigger,
Some(calculator),
)
.await; .await;
} }

View File

@ -69,11 +69,16 @@ impl EntityBase for TNTEntity {
if fuse <= 1 { if fuse <= 1 {
// TNT explodes now // TNT explodes now
self.entity.remove().await; self.entity.remove().await;
self.entity let world = self.entity.world.load();
.world if world.level_info.load().game_rules.tnt_explodes {
.load() world
.explode(self.entity.pos.load(), self.power) .explode(
self.entity.pos.load(),
self.power,
crate::world::ExplosionInteraction::Tnt,
)
.await; .await;
}
} else { } else {
// Safe decrement // Safe decrement
self.fuse.store(fuse - 1, Relaxed); self.fuse.store(fuse - 1, Relaxed);

View File

@ -99,7 +99,9 @@ impl TntMinecart {
if primed { if primed {
world.explode_tnt_minecart(pos, power).await; world.explode_tnt_minecart(pos, power).await;
} else { } else {
world.explode(pos, power).await; world
.explode(pos, power, crate::world::ExplosionInteraction::Tnt)
.await;
} }
} }

View File

@ -76,7 +76,7 @@ use crate::plugin::loader::wasm::wasm_host::{
}, },
wit::v0_1::pumpkin::{self, plugin::world::World}, wit::v0_1::pumpkin::{self, plugin::world::World},
}; };
use crate::world::explosion::Explosion; use crate::world::explosion::ExplosionInteraction;
use pumpkin_data::game_rules::{GameRule, GameRuleValue}; use pumpkin_data::game_rules::{GameRule, GameRuleValue};
pub(crate) fn from_wit_game_rule(rule: WitGameRule) -> GameRule { pub(crate) fn from_wit_game_rule(rule: WitGameRule) -> GameRule {
@ -682,15 +682,24 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
pos: pumpkin::plugin::common::Position, pos: pumpkin::plugin::common::Position,
power: f32, power: f32,
_create_fire: bool, _create_fire: bool,
_interaction: pumpkin::plugin::world::ExplosionInteraction, interaction: pumpkin::plugin::world::ExplosionInteraction,
) -> wasmtime::Result<()> { ) -> wasmtime::Result<()> {
let world_ref = self.get_world_res(&world)?; let world_ref = self.get_world_res(&world)?;
// Currently Explosion only supports power and position in this codebase let interaction = match interaction {
let explosion = Explosion::new( pumpkin::plugin::world::ExplosionInteraction::None => ExplosionInteraction::None,
power, pumpkin::plugin::world::ExplosionInteraction::Block => ExplosionInteraction::Block,
pumpkin::plugin::world::ExplosionInteraction::Mob => ExplosionInteraction::Mob,
pumpkin::plugin::world::ExplosionInteraction::Tnt => ExplosionInteraction::Tnt,
pumpkin::plugin::world::ExplosionInteraction::Trigger => ExplosionInteraction::Trigger,
};
world_ref
.provider
.explode(
pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2), pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2),
); power,
explosion.explode(&world_ref.provider).await; interaction,
)
.await;
Ok(()) Ok(())
} }

View File

@ -1,7 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use pumpkin_data::{ use pumpkin_data::{
Block, BlockState, BlockStateId, damage::DamageType, entity::EntityType, fluid::Fluid, Block, BlockState, BlockStateId,
damage::DamageType,
entity::EntityType,
fluid::Fluid,
tag::{Tag, Taggable},
}; };
use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}; use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3};
use pumpkin_world::chunk::ChunkData; use pumpkin_world::chunk::ChunkData;
@ -15,22 +19,200 @@ use crate::{
use super::{BlockFlags, World}; use super::{BlockFlags, World};
/// Defines the type of explosion interaction with the world.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExplosionInteraction {
None,
Block,
Mob,
Tnt,
Trigger,
}
/// Defines how an explosion interacts with blocks in the world.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BlockInteraction {
/// Keeps blocks intact (no block damage, no drops).
Keep,
/// Destroys blocks and drops 100% of items without decay.
Destroy,
/// Destroys blocks and applies loot decay based on explosion radius.
DestroyWithDecay,
/// Triggers block effects without destroying them.
TriggerBlock,
}
/// Defines how damage and block destruction are calculated for an explosion.
pub trait ExplosionDamageCalculator: Send + Sync {
/// Returns the block's explosion resistance. If None, the block is treated as air/empty.
fn get_block_explosion_resistance(
&self,
_explosion: &Explosion,
_world: &World,
_pos: &BlockPos,
block: &Block,
fluid: &pumpkin_data::fluid::FluidState,
) -> Option<f32> {
if block.default_state.is_air() && fluid.is_empty {
None
} else {
Some(fluid.blast_resistance.max(block.blast_resistance))
}
}
/// Returns whether this block should be destroyed / affected by the explosion.
fn should_block_explode(
&self,
_explosion: &Explosion,
_world: &World,
_pos: &BlockPos,
_block: &Block,
_power: f32,
) -> bool {
true
}
/// Returns whether the entity should take damage from the explosion.
fn should_damage_entity(&self, _explosion: &Explosion, _entity: &dyn EntityBase) -> bool {
true
}
/// Returns knockback multiplier for the given entity (default 1.0).
fn get_knockback_multiplier(&self, _entity: &dyn EntityBase) -> f32 {
1.0
}
/// Calculates the damage amount to deal to the entity given the exposure.
fn get_entity_damage_amount(
&self,
explosion: &Explosion,
entity: &dyn EntityBase,
exposure: f32,
) -> f32 {
let radius = explosion.power as f64 * 2.0;
let distance = (entity
.get_entity()
.pos
.load()
.squared_distance_to_vec(&explosion.pos))
.sqrt()
/ radius;
let damage_multiplier = (1.0 - distance) * exposure as f64;
(f64::midpoint(damage_multiplier * damage_multiplier, damage_multiplier)
* 7.0
* explosion.power as f64
+ 1.0) as f32
}
}
/// Default explosion damage calculator implementing vanilla standard explosion rules.
pub struct DefaultExplosionDamageCalculator;
impl ExplosionDamageCalculator for DefaultExplosionDamageCalculator {}
/// A configurable explosion damage calculator (e.g. for wind charges, mace wind bursts).
pub struct SimpleExplosionDamageCalculator {
pub damages_entities: bool,
pub damages_blocks: bool,
pub knockback_multiplier: Option<f32>,
pub immune_blocks: Option<&'static Tag>,
}
impl SimpleExplosionDamageCalculator {
#[must_use]
pub const fn new(
damages_entities: bool,
damages_blocks: bool,
knockback_multiplier: Option<f32>,
immune_blocks: Option<&'static Tag>,
) -> Self {
Self {
damages_entities,
damages_blocks,
knockback_multiplier,
immune_blocks,
}
}
}
impl ExplosionDamageCalculator for SimpleExplosionDamageCalculator {
fn get_block_explosion_resistance(
&self,
_explosion: &Explosion,
_world: &World,
_pos: &BlockPos,
block: &Block,
fluid: &pumpkin_data::fluid::FluidState,
) -> Option<f32> {
if let Some(immune_tag) = self.immune_blocks
&& block.has_tag(immune_tag)
{
return None;
}
if block.default_state.is_air() && fluid.is_empty {
None
} else {
Some(fluid.blast_resistance.max(block.blast_resistance))
}
}
fn should_block_explode(
&self,
_explosion: &Explosion,
_world: &World,
_pos: &BlockPos,
block: &Block,
_power: f32,
) -> bool {
if !self.damages_blocks {
return false;
}
if let Some(immune_tag) = self.immune_blocks
&& block.has_tag(immune_tag)
{
return false;
}
true
}
fn should_damage_entity(&self, _explosion: &Explosion, _entity: &dyn EntityBase) -> bool {
self.damages_entities
}
fn get_knockback_multiplier(&self, _entity: &dyn EntityBase) -> f32 {
self.knockback_multiplier.unwrap_or(1.0)
}
}
pub struct Explosion { pub struct Explosion {
power: f32, power: f32,
pos: Vector3<f64>, pos: Vector3<f64>,
block_interaction: BlockInteraction,
damage_calculator: Option<Arc<dyn ExplosionDamageCalculator>>,
preserve_rails: bool, preserve_rails: bool,
} }
impl Explosion { impl Explosion {
#[must_use] #[must_use]
pub const fn new(power: f32, pos: Vector3<f64>) -> Self { pub const fn new(power: f32, pos: Vector3<f64>, block_interaction: BlockInteraction) -> Self {
Self { Self {
power, power,
pos, pos,
block_interaction,
damage_calculator: None,
preserve_rails: false, preserve_rails: false,
} }
} }
#[must_use]
pub fn with_damage_calculator(
mut self,
calculator: Arc<dyn ExplosionDamageCalculator>,
) -> Self {
self.damage_calculator = Some(calculator);
self
}
#[must_use] #[must_use]
pub const fn preserving_rails(mut self) -> Self { pub const fn preserving_rails(mut self) -> Self {
self.preserve_rails = true; self.preserve_rails = true;
@ -48,6 +230,7 @@ impl Explosion {
|| block.id == Block::ACTIVATOR_RAIL.id || block.id == Block::ACTIVATOR_RAIL.id
} }
#[allow(clippy::too_many_lines)]
fn get_blocks_to_destroy( fn get_blocks_to_destroy(
&self, &self,
world: &World, world: &World,
@ -59,6 +242,12 @@ impl Explosion {
Option<Arc<ChunkData>>, Option<Arc<ChunkData>>,
> = FxHashMap::default(); > = FxHashMap::default();
let default_calc = DefaultExplosionDamageCalculator;
let calc: &dyn ExplosionDamageCalculator = match &self.damage_calculator {
Some(c) => c.as_ref(),
None => &default_calc,
};
for x in 0..16 { for x in 0..16 {
for y in 0..16 { for y in 0..16 {
for z in 0..16 { for z in 0..16 {
@ -137,14 +326,25 @@ impl Explosion {
if !state.is_air() || !fluid_state.is_empty { if !state.is_air() || !fluid_state.is_empty {
let protects_rail = self.protects_rail(world, &block_pos, block); let protects_rail = self.protects_rail(world, &block_pos, block);
let resistance = if protects_rail { let resistance = if protects_rail {
0.0 Some(0.0)
} else { } else {
fluid_state.blast_resistance.max(block.blast_resistance) calc.get_block_explosion_resistance(
self,
world,
&block_pos,
block,
fluid_state,
)
}; };
if let Some(resistance) = resistance {
h -= (resistance + 0.3) * 0.3; h -= (resistance + 0.3) * 0.3;
}
if h > 0.0 && !protects_rail { if h > 0.0
&& !protects_rail
&& calc.should_block_explode(self, world, &block_pos, block, h)
{
map.insert(block_pos, (block, state)); map.insert(block_pos, (block, state));
} }
} }
@ -181,6 +381,12 @@ impl Explosion {
let entities = world.get_all_at_box(&search_box); let entities = world.get_all_at_box(&search_box);
let default_calc = DefaultExplosionDamageCalculator;
let calc: &dyn ExplosionDamageCalculator = match &self.damage_calculator {
Some(c) => c.as_ref(),
None => &default_calc,
};
for entity_base in entities { for entity_base in entities {
if entity_base.is_immune_to_explosion() { if entity_base.is_immune_to_explosion() {
continue; continue;
@ -198,21 +404,26 @@ impl Explosion {
continue; continue;
} }
let exposure = Self::calculate_exposure(&self.pos, entity, world).await as f64; let should_damage = calc.should_damage_entity(self, entity_base.as_ref());
let knockback_multiplier = calc.get_knockback_multiplier(entity_base.as_ref()) as f64;
let exposure = if !should_damage && knockback_multiplier == 0.0 {
0.0
} else {
Self::calculate_exposure(&self.pos, entity, world).await as f64
};
if exposure == 0.0 { if exposure == 0.0 {
continue; continue;
} }
let damage_multiplier = (1.0 - distance) * exposure; if should_damage {
let damage = (f64::midpoint(damage_multiplier * damage_multiplier, damage_multiplier) let damage =
* 7.0 calc.get_entity_damage_amount(self, entity_base.as_ref(), exposure as f32);
* self.power as f64
+ 1.0) as f32;
// TODO: damage type
entity entity
.damage(entity_base.as_ref(), damage, DamageType::EXPLOSION) .damage(entity_base.as_ref(), damage, DamageType::EXPLOSION)
.await; .await;
}
// Calculate and apply knockback // Calculate and apply knockback
let dir_pos = if entity.entity_type == &EntityType::TNT { let dir_pos = if entity.entity_type == &EntityType::TNT {
@ -221,11 +432,12 @@ impl Explosion {
entity.get_eye_pos() entity.get_eye_pos()
}; };
let direction = (dir_pos - self.pos).normalize(); let direction = (dir_pos - self.pos).normalize();
// TODO // TODO: entity explosion knockback resistance attribute
let knockback_resistance = 0.0; let knockback_resistance = 0.0;
let knockback_multiplier = (1.0 - distance) * exposure * (1.0 - knockback_resistance); let knockback_power =
let knockback = direction * knockback_multiplier; (1.0 - distance) * exposure * knockback_multiplier * (1.0 - knockback_resistance);
let knockback = direction * knockback_power;
entity.add_velocity(knockback); entity.add_velocity(knockback);
} }
} }
@ -291,8 +503,30 @@ impl Explosion {
/// Returns the removed block count /// Returns the removed block count
pub async fn explode(&self, world: &Arc<World>) -> u32 { pub async fn explode(&self, world: &Arc<World>) -> u32 {
self.damage_entities(world).await;
match self.block_interaction {
BlockInteraction::Keep => 0,
BlockInteraction::TriggerBlock => {
let blocks = self.get_blocks_to_destroy(world);
for (pos, (block, _state)) in &blocks {
let pumpkin_block = world.block_registry.get_pumpkin_block(block.id);
if let Some(pumpkin_block) = pumpkin_block {
pumpkin_block
.explode(ExplodeArgs {
world,
block,
position: pos,
})
.await;
}
}
0
}
BlockInteraction::Destroy | BlockInteraction::DestroyWithDecay => {
let center_pos = BlockPos::floored(self.pos.x, self.pos.y, self.pos.z); let center_pos = BlockPos::floored(self.pos.x, self.pos.y, self.pos.z);
let mut event = crate::plugin::api::events::block::block_explode::BlockExplodeEvent::new( let mut event =
crate::plugin::api::events::block::block_explode::BlockExplodeEvent::new(
center_pos, center_pos,
if self.power > 0.0 { if self.power > 0.0 {
1.0 / self.power 1.0 / self.power
@ -308,7 +542,9 @@ impl Explosion {
} }
let blocks = self.get_blocks_to_destroy(world); let blocks = self.get_blocks_to_destroy(world);
self.damage_entities(world).await; let decay_drops = self.block_interaction == BlockInteraction::DestroyWithDecay;
let explosion_radius = decay_drops.then_some(self.power);
for (pos, (block, state)) in &blocks { for (pos, (block, state)) in &blocks {
world world
.set_block_state(pos, BlockStateId::AIR, BlockFlags::NOTIFY_ALL) .set_block_state(pos, BlockStateId::AIR, BlockFlags::NOTIFY_ALL)
@ -322,7 +558,7 @@ impl Explosion {
let is_thundering = world.is_thundering().await; let is_thundering = world.is_thundering().await;
let params = LootContextParameters { let params = LootContextParameters {
block_state: Some(state), block_state: Some(state),
explosion_radius: Some(self.power), explosion_radius,
position: Some(pumpkin_util::math::vector3::Vector3::new( position: Some(pumpkin_util::math::vector3::Vector3::new(
pos.0.x as f64, pos.0.x as f64,
pos.0.y as f64, pos.0.y as f64,
@ -348,6 +584,8 @@ impl Explosion {
// TODO: fire // TODO: fire
blocks.len() as u32 blocks.len() as u32
} }
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -53,7 +53,10 @@ use crate::{
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use border::Worldborder; use border::Worldborder;
use bytes::{BufMut, Bytes}; use bytes::{BufMut, Bytes};
use explosion::Explosion; pub use explosion::{
BlockInteraction, DefaultExplosionDamageCalculator, Explosion, ExplosionDamageCalculator,
ExplosionInteraction, SimpleExplosionDamageCalculator,
};
use pumpkin_config::BasicConfiguration; use pumpkin_config::BasicConfiguration;
use pumpkin_data::block_properties::{blocks_movement, is_air}; use pumpkin_data::block_properties::{blocks_movement, is_air};
use pumpkin_data::block_rotation::{Mirror, Rotation}; use pumpkin_data::block_rotation::{Mirror, Rotation};
@ -3797,16 +3800,68 @@ impl World {
player.set_health(20.0).await; player.set_health(20.0).await;
} }
pub async fn explode(self: &Arc<Self>, position: Vector3<f64>, power: f32) { pub async fn explode(
let explosion = Explosion::new(power, position); self: &Arc<Self>,
position: Vector3<f64>,
power: f32,
interaction: ExplosionInteraction,
) {
self.explode_with_calculator(position, power, interaction, None)
.await;
}
pub async fn explode_with_calculator(
self: &Arc<Self>,
position: Vector3<f64>,
power: f32,
interaction: ExplosionInteraction,
damage_calculator: Option<Arc<dyn ExplosionDamageCalculator>>,
) {
let block_interaction = self.get_block_interaction(interaction);
let mut explosion = Explosion::new(power, position, block_interaction);
if let Some(calc) = damage_calculator {
explosion = explosion.with_damage_calculator(calc);
}
self.run_explosion(explosion, position, power).await; self.run_explosion(explosion, position, power).await;
} }
pub async fn explode_tnt_minecart(self: &Arc<Self>, position: Vector3<f64>, power: f32) { pub async fn explode_tnt_minecart(self: &Arc<Self>, position: Vector3<f64>, power: f32) {
let explosion = Explosion::new(power, position).preserving_rails(); let block_interaction = self.get_block_interaction(ExplosionInteraction::Tnt);
let explosion = Explosion::new(power, position, block_interaction).preserving_rails();
self.run_explosion(explosion, position, power).await; self.run_explosion(explosion, position, power).await;
} }
#[must_use]
pub fn get_block_interaction(&self, interaction: ExplosionInteraction) -> BlockInteraction {
let game_rules = &self.level_info.load().game_rules;
match interaction {
ExplosionInteraction::None => BlockInteraction::Keep,
ExplosionInteraction::Block => {
Self::get_destroy_type(game_rules.block_explosion_drop_decay)
}
ExplosionInteraction::Mob => {
if game_rules.mob_griefing {
Self::get_destroy_type(game_rules.mob_explosion_drop_decay)
} else {
BlockInteraction::Keep
}
}
ExplosionInteraction::Tnt => {
Self::get_destroy_type(game_rules.tnt_explosion_drop_decay)
}
ExplosionInteraction::Trigger => BlockInteraction::TriggerBlock,
}
}
#[must_use]
pub const fn get_destroy_type(drop_decay: bool) -> BlockInteraction {
if drop_decay {
BlockInteraction::DestroyWithDecay
} else {
BlockInteraction::Destroy
}
}
async fn run_explosion( async fn run_explosion(
self: &Arc<Self>, self: &Arc<Self>,
explosion: Explosion, explosion: Explosion,