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;
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;
return BlockActionResult::SuccessServer;

View File

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

View File

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

View File

@@ -115,7 +115,13 @@ impl CreeperEntity {
.store(true, Ordering::Relaxed);
let world = entity.world.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
entity.remove().await;
}

View File

@@ -272,7 +272,13 @@ impl EntityBase for FireballEntity {
}
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,
}
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 {
#[must_use]
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>) {
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()
.world
.load()
.explode(position, EXPLOSION_POWER)
.explode_with_calculator(
position,
EXPLOSION_POWER,
crate::world::ExplosionInteraction::Trigger,
Some(calculator),
)
.await;
}

View File

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

View File

@@ -99,7 +99,9 @@ impl TntMinecart {
if primed {
world.explode_tnt_minecart(pos, power).await;
} 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},
};
use crate::world::explosion::Explosion;
use crate::world::explosion::ExplosionInteraction;
use pumpkin_data::game_rules::{GameRule, GameRuleValue};
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,
power: f32,
_create_fire: bool,
_interaction: pumpkin::plugin::world::ExplosionInteraction,
interaction: pumpkin::plugin::world::ExplosionInteraction,
) -> wasmtime::Result<()> {
let world_ref = self.get_world_res(&world)?;
// Currently Explosion only supports power and position in this codebase
let explosion = Explosion::new(
power,
pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2),
);
explosion.explode(&world_ref.provider).await;
let interaction = match interaction {
pumpkin::plugin::world::ExplosionInteraction::None => ExplosionInteraction::None,
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),
power,
interaction,
)
.await;
Ok(())
}

View File

@@ -1,7 +1,11 @@
use std::sync::Arc;
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_world::chunk::ChunkData;
@@ -15,22 +19,200 @@ use crate::{
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 {
power: f32,
pos: Vector3<f64>,
block_interaction: BlockInteraction,
damage_calculator: Option<Arc<dyn ExplosionDamageCalculator>>,
preserve_rails: bool,
}
impl Explosion {
#[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 {
power,
pos,
block_interaction,
damage_calculator: None,
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]
pub const fn preserving_rails(mut self) -> Self {
self.preserve_rails = true;
@@ -48,6 +230,7 @@ impl Explosion {
|| block.id == Block::ACTIVATOR_RAIL.id
}
#[allow(clippy::too_many_lines)]
fn get_blocks_to_destroy(
&self,
world: &World,
@@ -59,6 +242,12 @@ impl Explosion {
Option<Arc<ChunkData>>,
> = 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 y in 0..16 {
for z in 0..16 {
@@ -137,14 +326,25 @@ impl Explosion {
if !state.is_air() || !fluid_state.is_empty {
let protects_rail = self.protects_rail(world, &block_pos, block);
let resistance = if protects_rail {
0.0
Some(0.0)
} else {
fluid_state.blast_resistance.max(block.blast_resistance)
calc.get_block_explosion_resistance(
self,
world,
&block_pos,
block,
fluid_state,
)
};
h -= (resistance + 0.3) * 0.3;
if let Some(resistance) = resistance {
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));
}
}
@@ -181,6 +381,12 @@ impl Explosion {
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 {
if entity_base.is_immune_to_explosion() {
continue;
@@ -198,21 +404,26 @@ impl Explosion {
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 {
continue;
}
let damage_multiplier = (1.0 - distance) * exposure;
let damage = (f64::midpoint(damage_multiplier * damage_multiplier, damage_multiplier)
* 7.0
* self.power as f64
+ 1.0) as f32;
// TODO: damage type
entity
.damage(entity_base.as_ref(), damage, DamageType::EXPLOSION)
.await;
if should_damage {
let damage =
calc.get_entity_damage_amount(self, entity_base.as_ref(), exposure as f32);
entity
.damage(entity_base.as_ref(), damage, DamageType::EXPLOSION)
.await;
}
// Calculate and apply knockback
let dir_pos = if entity.entity_type == &EntityType::TNT {
@@ -221,11 +432,12 @@ impl Explosion {
entity.get_eye_pos()
};
let direction = (dir_pos - self.pos).normalize();
// TODO
// TODO: entity explosion knockback resistance attribute
let knockback_resistance = 0.0;
let knockback_multiplier = (1.0 - distance) * exposure * (1.0 - knockback_resistance);
let knockback = direction * knockback_multiplier;
let knockback_power =
(1.0 - distance) * exposure * knockback_multiplier * (1.0 - knockback_resistance);
let knockback = direction * knockback_power;
entity.add_velocity(knockback);
}
}
@@ -291,62 +503,88 @@ impl Explosion {
/// Returns the removed block count
pub async fn explode(&self, world: &Arc<World>) -> u32 {
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(
center_pos,
if self.power > 0.0 {
1.0 / self.power
} else {
1.0
},
);
if let Some(server) = world.server.upgrade() {
server.plugin_manager.fire(&server, &mut event).await;
}
if event.cancelled {
return 0;
}
let blocks = self.get_blocks_to_destroy(world);
self.damage_entities(world).await;
for (pos, (block, state)) in &blocks {
world
.set_block_state(pos, BlockStateId::AIR, BlockFlags::NOTIFY_ALL)
.await;
world.close_container_screens_at(pos).await;
let pumpkin_block = world.block_registry.get_pumpkin_block(block.id);
if pumpkin_block.is_none_or(|s| s.should_drop_items_on_explosion()) {
let is_raining = world.is_raining().await;
let is_thundering = world.is_thundering().await;
let params = LootContextParameters {
block_state: Some(state),
explosion_radius: Some(self.power),
position: Some(pumpkin_util::math::vector3::Vector3::new(
pos.0.x as f64,
pos.0.y as f64,
pos.0.z as f64,
)),
world_time: world.level_info.load().day_time as u64,
is_raining: Some(is_raining),
is_thundering: Some(is_thundering),
..Default::default()
};
drop_loot(world, block, pos, false, params).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
}
if let Some(pumpkin_block) = pumpkin_block {
pumpkin_block
.explode(ExplodeArgs {
world,
block,
position: pos,
})
.await;
BlockInteraction::Destroy | BlockInteraction::DestroyWithDecay => {
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(
center_pos,
if self.power > 0.0 {
1.0 / self.power
} else {
1.0
},
);
if let Some(server) = world.server.upgrade() {
server.plugin_manager.fire(&server, &mut event).await;
}
if event.cancelled {
return 0;
}
let blocks = self.get_blocks_to_destroy(world);
let decay_drops = self.block_interaction == BlockInteraction::DestroyWithDecay;
let explosion_radius = decay_drops.then_some(self.power);
for (pos, (block, state)) in &blocks {
world
.set_block_state(pos, BlockStateId::AIR, BlockFlags::NOTIFY_ALL)
.await;
world.close_container_screens_at(pos).await;
let pumpkin_block = world.block_registry.get_pumpkin_block(block.id);
if pumpkin_block.is_none_or(|s| s.should_drop_items_on_explosion()) {
let is_raining = world.is_raining().await;
let is_thundering = world.is_thundering().await;
let params = LootContextParameters {
block_state: Some(state),
explosion_radius,
position: Some(pumpkin_util::math::vector3::Vector3::new(
pos.0.x as f64,
pos.0.y as f64,
pos.0.z as f64,
)),
world_time: world.level_info.load().day_time as u64,
is_raining: Some(is_raining),
is_thundering: Some(is_thundering),
..Default::default()
};
drop_loot(world, block, pos, false, params).await;
}
if let Some(pumpkin_block) = pumpkin_block {
pumpkin_block
.explode(ExplodeArgs {
world,
block,
position: pos,
})
.await;
}
}
// TODO: fire
blocks.len() as u32
}
}
// TODO: fire
blocks.len() as u32
}
}

View File

@@ -53,7 +53,10 @@ use crate::{
use arc_swap::ArcSwap;
use border::Worldborder;
use bytes::{BufMut, Bytes};
use explosion::Explosion;
pub use explosion::{
BlockInteraction, DefaultExplosionDamageCalculator, Explosion, ExplosionDamageCalculator,
ExplosionInteraction, SimpleExplosionDamageCalculator,
};
use pumpkin_config::BasicConfiguration;
use pumpkin_data::block_properties::{blocks_movement, is_air};
use pumpkin_data::block_rotation::{Mirror, Rotation};
@@ -3797,16 +3800,68 @@ impl World {
player.set_health(20.0).await;
}
pub async fn explode(self: &Arc<Self>, position: Vector3<f64>, power: f32) {
let explosion = Explosion::new(power, position);
pub async fn explode(
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;
}
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;
}
#[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(
self: &Arc<Self>,
explosion: Explosion,