Skip to main content

steel_core/entity/entities/objects/explosives/
primed_tnt.rs

1//! Primed TNT entity behavior.
2
3use std::{
4    f32::consts::TAU,
5    sync::{Arc, Weak},
6};
7
8use glam::DVec3;
9use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
10use simdnbt::owned::NbtCompound;
11use steel_macros::entity_behavior;
12use steel_registry::blocks::block_state_ext::BlockStateExt as _;
13use steel_registry::entity_type::EntityTypeRef;
14use steel_registry::fluid::FluidState;
15use steel_registry::vanilla_blocks;
16use steel_registry::vanilla_entity_data::PrimedTntEntityData;
17use steel_registry::vanilla_game_rules::TNT_EXPLODES;
18use steel_utils::locks::SyncMutex;
19use steel_utils::nbt::NbtValueInput as _;
20use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey};
21
22use crate::block_entity::block_state_nbt;
23use crate::entity::damage::DamageSource;
24use crate::entity::{
25    Entity, EntityBase, EntityBaseLoad, EntityMovementEmission, EntityReference, EntitySyncedData,
26    RemovalReason, SharedEntity,
27};
28use crate::physics::MoverType;
29use crate::world::{
30    DefaultExplosionDamageCalculator, Explosion, ExplosionBlockReader, ExplosionDamageCalculator,
31    ExplosionInteraction, ExplosionOptions, ImmutableExplosionBlockCalculator, World,
32};
33
34const DEFAULT_EXPLOSION_POWER: f32 = 4.0;
35const MAX_EXPLOSION_POWER: f32 = 128.0;
36const GRAVITY: f64 = 0.04;
37const AIR_DRAG: f32 = 0.98;
38const INITIAL_HORIZONTAL_SPEED: f64 = 0.02;
39const INITIAL_VERTICAL_SPEED: f32 = 0.2;
40const EXPLOSION_HEIGHT_FRACTION: f32 = 0.0625;
41const GROUND_HORIZONTAL_DRAG: f64 = 0.7;
42const GROUND_VERTICAL_BOUNCE: f64 = -0.5;
43
44struct PrimedTntState {
45    owner: Option<EntityReference>,
46    used_portal: bool,
47    explosion_power: f32,
48}
49
50impl PrimedTntState {
51    const fn new() -> Self {
52        Self {
53            owner: None,
54            used_portal: false,
55            explosion_power: DEFAULT_EXPLOSION_POWER,
56        }
57    }
58}
59
60struct NetherPortalPreservingExplosionDamageCalculator;
61
62impl ExplosionDamageCalculator for NetherPortalPreservingExplosionDamageCalculator {
63    fn block_explosion_resistance(
64        &self,
65        explosion: &dyn Explosion,
66        world: &World,
67        pos: BlockPos,
68        state: BlockStateId,
69        fluid: FluidState,
70    ) -> Option<f32> {
71        if state.get_block() == &vanilla_blocks::NETHER_PORTAL {
72            return None;
73        }
74        DefaultExplosionDamageCalculator
75            .block_explosion_resistance(explosion, world, pos, state, fluid)
76    }
77
78    fn should_block_explode(
79        &self,
80        explosion: &dyn Explosion,
81        world: &World,
82        pos: BlockPos,
83        state: BlockStateId,
84        power: f32,
85    ) -> bool {
86        if state.get_block() == &vanilla_blocks::NETHER_PORTAL {
87            return false;
88        }
89        DefaultExplosionDamageCalculator.should_block_explode(explosion, world, pos, state, power)
90    }
91}
92
93impl ImmutableExplosionBlockCalculator for NetherPortalPreservingExplosionDamageCalculator {
94    fn bounded_block_read_radius(&self) -> Option<u32> {
95        Some(0)
96    }
97
98    fn can_cache_explosion_resistance(&self) -> bool {
99        true
100    }
101
102    fn explosion_resistance(
103        &self,
104        reader: &dyn ExplosionBlockReader,
105        pos: BlockPos,
106        state: BlockStateId,
107        fluid: FluidState,
108    ) -> Option<f32> {
109        if state.get_block() == &vanilla_blocks::NETHER_PORTAL {
110            return None;
111        }
112        DefaultExplosionDamageCalculator.explosion_resistance(reader, pos, state, fluid)
113    }
114
115    fn should_explode(
116        &self,
117        reader: &dyn ExplosionBlockReader,
118        pos: BlockPos,
119        state: BlockStateId,
120        power: f32,
121    ) -> bool {
122        if state.get_block() == &vanilla_blocks::NETHER_PORTAL {
123            return false;
124        }
125        DefaultExplosionDamageCalculator.should_explode(reader, pos, state, power)
126    }
127}
128
129static USED_PORTAL_DAMAGE_CALCULATOR: NetherPortalPreservingExplosionDamageCalculator =
130    NetherPortalPreservingExplosionDamageCalculator;
131
132/// Vanilla primed TNT entity.
133#[entity_behavior(class = "PrimedTnt")]
134pub struct PrimedTntEntity {
135    base: EntityBase,
136    entity_type: EntityTypeRef,
137    entity_data: SyncMutex<PrimedTntEntityData>,
138    state: SyncMutex<PrimedTntState>,
139}
140
141// SAFETY: This key is owned by Steel and uniquely identifies `PrimedTntEntity`.
142unsafe impl DowncastType for PrimedTntEntity {
143    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/primed_tnt");
144}
145
146impl PrimedTntEntity {
147    /// Default fuse duration in ticks.
148    pub const DEFAULT_FUSE_TIME: i32 = 80;
149
150    /// Creates an unprimed TNT entity for the entity factory.
151    #[must_use]
152    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
153        Self {
154            base: EntityBase::new(id, position, entity_type.dimensions, world),
155            entity_type,
156            entity_data: SyncMutex::new(PrimedTntEntityData::new()),
157            state: SyncMutex::new(PrimedTntState::new()),
158        }
159    }
160
161    /// Creates a normally primed TNT entity with Vanilla's randomized launch motion.
162    #[must_use]
163    pub fn primed(
164        entity_type: EntityTypeRef,
165        id: i32,
166        position: DVec3,
167        world: &Arc<World>,
168        owner: Option<&dyn Entity>,
169    ) -> Self {
170        let entity = Self::new(entity_type, id, position, Arc::downgrade(world));
171        entity.set_velocity(Self::initial_velocity(rand::random()));
172        entity.state.lock().owner = owner.map(|owner| EntityReference::from_uuid(owner.uuid()));
173        entity
174    }
175
176    fn initial_velocity(random: f64) -> DVec3 {
177        // Vanilla narrows 2π to float before multiplying by the random sample.
178        let angle = random * f64::from(TAU);
179        DVec3::new(
180            -angle.sin() * INITIAL_HORIZONTAL_SPEED,
181            f64::from(INITIAL_VERTICAL_SPEED),
182            -angle.cos() * INITIAL_HORIZONTAL_SPEED,
183        )
184    }
185
186    /// Creates a primed TNT entity from saved data.
187    #[must_use]
188    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
189        Self {
190            base: EntityBase::from_load(load, entity_type.dimensions),
191            entity_type,
192            entity_data: SyncMutex::new(PrimedTntEntityData::new()),
193            state: SyncMutex::new(PrimedTntState::new()),
194        }
195    }
196
197    /// Returns the remaining fuse time in ticks.
198    #[must_use]
199    pub fn fuse(&self) -> i32 {
200        *self.entity_data.lock().fuse.get()
201    }
202
203    /// Sets the remaining fuse time in ticks.
204    pub fn set_fuse(&self, fuse: i32) {
205        self.entity_data.lock().fuse.set(fuse);
206    }
207
208    fn decrement_fuse(&self) -> i32 {
209        let mut entity_data = self.entity_data.lock();
210        let fuse = *entity_data.fuse.get() - 1;
211        entity_data.fuse.set(fuse);
212        fuse
213    }
214
215    /// Returns the block state rendered by the client.
216    #[must_use]
217    pub fn block_state(&self) -> BlockStateId {
218        *self.entity_data.lock().block_state.get()
219    }
220
221    /// Sets the block state rendered by the client.
222    pub fn set_block_state(&self, state: BlockStateId) {
223        self.entity_data.lock().block_state.set(state);
224    }
225
226    /// Returns Vanilla's shortened chain-reaction fuse for `fuse`.
227    /// Adds one eighth of the fuse to a random duration below one quarter,
228    /// with a minimum random bound of one tick.
229    #[must_use]
230    pub fn get_random_short_fuse(fuse: i32) -> i32 {
231        rand::random_range(0..(fuse / 4).max(1)) + fuse / 8
232    }
233
234    fn owner_reference(&self) -> Option<EntityReference> {
235        self.state.lock().owner.clone()
236    }
237
238    fn owner(&self) -> Option<SharedEntity> {
239        let world = self.level()?;
240        self.owner_reference()?
241            .get_entity(&world)
242            .filter(|owner| owner.is_living_entity())
243    }
244
245    fn explode(self: &Arc<Self>, world: &Arc<World>) {
246        if !world.get_game_rule(&TNT_EXPLODES) {
247            return;
248        }
249
250        let position = self.position();
251        let center = DVec3::new(
252            position.x,
253            position.y
254                + f64::from(self.base.dimensions().height) * f64::from(EXPLOSION_HEIGHT_FRACTION),
255            position.z,
256        );
257        let (explosion_power, used_portal) = {
258            let state = self.state.lock();
259            (state.explosion_power, state.used_portal)
260        };
261        let mut options = ExplosionOptions::new(center, explosion_power, ExplosionInteraction::Tnt);
262        options.source = Some(Arc::clone(self) as SharedEntity);
263        if used_portal {
264            options.damage_calculator = Some(&USED_PORTAL_DAMAGE_CALCULATOR);
265            options.immutable_block_calculator = Some(&USED_PORTAL_DAMAGE_CALCULATOR);
266        } else {
267            options.immutable_block_calculator = Some(&DefaultExplosionDamageCalculator);
268        }
269        world.explode(options);
270    }
271}
272
273impl Entity for PrimedTntEntity {
274    fn base(&self) -> &EntityBase {
275        &self.base
276    }
277
278    fn entity_type(&self) -> EntityTypeRef {
279        self.entity_type
280    }
281
282    fn tick(self: Arc<Self>) {
283        self.handle_portal();
284        self.apply_gravity();
285        Arc::clone(&self).move_entity(MoverType::SelfMovement, self.velocity());
286        self.apply_effects_from_blocks();
287        let mut velocity = self.velocity() * f64::from(AIR_DRAG);
288        if self.on_ground() {
289            velocity = DVec3::new(
290                velocity.x * GROUND_HORIZONTAL_DRAG,
291                velocity.y * GROUND_VERTICAL_BOUNCE,
292                velocity.z * GROUND_HORIZONTAL_DRAG,
293            );
294        }
295        self.set_velocity(velocity);
296
297        let fuse = self.decrement_fuse();
298        if fuse <= 0 {
299            self.set_removed(RemovalReason::Discarded);
300            if let Some(world) = self.level() {
301                self.explode(&world);
302            }
303        } else {
304            self.update_fluid_interaction();
305        }
306
307        self.base.set_requires_precise_position(
308            self.horizontal_collision() || self.vertical_collision(),
309        );
310    }
311
312    fn blocks_building(&self) -> bool {
313        true
314    }
315
316    fn is_pickable(&self) -> bool {
317        !self.is_removed()
318    }
319
320    fn is_hard_collision_relevant(&self) -> bool {
321        false
322    }
323
324    fn movement_emission(&self) -> EntityMovementEmission {
325        EntityMovementEmission::None
326    }
327
328    fn get_default_gravity(&self) -> f64 {
329        GRAVITY
330    }
331
332    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
333        Some(&self.entity_data)
334    }
335
336    fn explosion_indirect_source(&self) -> Option<SharedEntity> {
337        self.owner()
338    }
339
340    fn explosion_damage_origin(&self) -> DVec3 {
341        self.position()
342    }
343
344    fn on_teleported(&self) {
345        self.state.lock().used_portal = true;
346    }
347
348    fn save_additional(&self, nbt: &mut NbtCompound) {
349        nbt.insert("fuse", self.fuse() as i16);
350        nbt.insert("block_state", block_state_nbt::save(self.block_state()));
351        let state = self.state.lock();
352        if state.explosion_power != DEFAULT_EXPLOSION_POWER {
353            nbt.insert("explosion_power", state.explosion_power);
354        }
355        if let Some(owner) = &state.owner {
356            owner.store(nbt, "owner");
357        }
358    }
359
360    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
361        self.set_fuse(i32::from(
362            nbt.short_value("fuse")
363                .unwrap_or(Self::DEFAULT_FUSE_TIME as i16),
364        ));
365        self.set_block_state(
366            nbt.compound("block_state")
367                .and_then(block_state_nbt::load)
368                .unwrap_or_else(|| vanilla_blocks::TNT.default_state()),
369        );
370        let mut state = self.state.lock();
371        state.explosion_power = nbt
372            .float_value("explosion_power")
373            .unwrap_or(DEFAULT_EXPLOSION_POWER)
374            .clamp(0.0, MAX_EXPLOSION_POWER);
375        state.owner = EntityReference::read(&nbt, "owner");
376    }
377
378    fn hurt(&self, _world: &World, _source: &DamageSource, _amount: f32) -> bool {
379        false
380    }
381}
382
383#[cfg(test)]
384#[path = "primed_tnt/tests.rs"]
385mod tests;