steel_core/entity/entities/objects/explosives/
primed_tnt.rs1use 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#[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
141unsafe impl DowncastType for PrimedTntEntity {
143 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/primed_tnt");
144}
145
146impl PrimedTntEntity {
147 pub const DEFAULT_FUSE_TIME: i32 = 80;
149
150 #[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 #[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 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 #[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 #[must_use]
199 pub fn fuse(&self) -> i32 {
200 *self.entity_data.lock().fuse.get()
201 }
202
203 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 #[must_use]
217 pub fn block_state(&self) -> BlockStateId {
218 *self.entity_data.lock().block_state.get()
219 }
220
221 pub fn set_block_state(&self, state: BlockStateId) {
223 self.entity_data.lock().block_state.set(state);
224 }
225
226 #[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;