1use std::sync::Arc;
2
3use rand::RngExt;
4use steel_macros::block_behavior;
5use steel_registry::blocks::block_state_ext::BlockStateExt;
6use steel_registry::blocks::properties::{
7 BlockStateProperties, BoolProperty, EnumProperty, IntProperty, SpeleothemThickness,
8};
9use steel_registry::blocks::shapes::{BooleanOp, VoxelShape, join_is_not_empty};
10use steel_registry::{
11 fluid::FluidRef, level_events, vanilla_block_tags::BlockTag, vanilla_blocks,
12 vanilla_damage_types, vanilla_entities, vanilla_fluids, vanilla_game_events,
13};
14use steel_utils::{BlockLocalAabb, BlockPos, BlockStateId, Direction, types::UpdateFlags};
15
16use crate::behavior::BLOCK_BEHAVIORS;
17use crate::behavior::block::{
18 BlockBehavior, BlockCollisionContext, EntityFallDamage, EntityFallOnContext, push_entities_up,
19 schedule_water_tick_if_waterlogged,
20};
21use crate::behavior::context::BlockPlaceContext;
22use crate::entity::damage::DamageSource;
23use crate::entity::entities::FallingBlockEntity;
24use crate::entity::projectile::Projectile;
25use crate::fluid::FluidStateExt as _;
26use crate::world::game_event::GameEventContext;
27use crate::world::{ClipHitResult, World};
28use crate::world::{LevelReader, ScheduledTickAccess};
29
30use super::BlockRef;
31
32#[block_behavior]
38pub struct PointedDripstoneBlock {
39 block: BlockRef,
40}
41
42const LEVEL_CAULDRON: &IntProperty = &BlockStateProperties::LEVEL_CAULDRON;
43const SPELEOTHEM_THICKNESS: &EnumProperty<SpeleothemThickness> =
44 &BlockStateProperties::SPELEOTHEM_THICKNESS;
45const VERTICAL_DIRECTION: &EnumProperty<Direction> = &BlockStateProperties::VERTICAL_DIRECTION;
46const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
47
48impl PointedDripstoneBlock {
49 #[must_use]
51 pub const fn new(block: BlockRef) -> Self {
52 Self { block }
53 }
54
55 fn fall_damage_for_state(state: BlockStateId, fall_distance: f64) -> Option<EntityFallDamage> {
56 if state.get_value(VERTICAL_DIRECTION) != Direction::Up
57 || state.get_value(SPELEOTHEM_THICKNESS) != SpeleothemThickness::Tip
58 {
59 return None;
60 }
61
62 Some(EntityFallDamage::new(
63 fall_distance + 2.5,
64 2.0,
65 DamageSource::environment(&vanilla_damage_types::STALAGMITE),
66 ))
67 }
68
69 const fn speleothem(&self) -> SpeleothemBlockBehavior {
70 SpeleothemBlockBehavior {
71 block: self.block,
72 kind: SpeleothemKind::PointedDripstone,
73 }
74 }
75}
76
77impl BlockBehavior for PointedDripstoneBlock {
78 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
79 self.speleothem().can_survive(state, world, pos)
80 }
81
82 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
83 self.speleothem().state_for_placement(context)
84 }
85
86 fn update_shape(
87 &self,
88 state: BlockStateId,
89 world: &dyn ScheduledTickAccess,
90 pos: BlockPos,
91 direction: Direction,
92 neighbor_pos: BlockPos,
93 neighbor_state: BlockStateId,
94 ) -> BlockStateId {
95 self.speleothem()
96 .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
97 }
98
99 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
100 self.speleothem().tick(state, world, pos);
101 }
102
103 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
104 self.speleothem().random_tick(state, world, pos);
105 }
106
107 fn fall_on(
108 &self,
109 state: BlockStateId,
110 world: &Arc<World>,
111 pos: BlockPos,
112 context: EntityFallOnContext<'_>,
113 ) -> Option<EntityFallDamage> {
114 Self::fall_damage_for_state(state, context.fall_distance)
115 .or_else(|| self.default_fall_on(state, world, pos, context))
116 }
117
118 fn on_projectile_hit(
119 &self,
120 _state: BlockStateId,
121 world: &Arc<World>,
122 hit: &ClipHitResult,
123 projectile: &dyn Projectile,
124 ) {
125 SpeleothemBlockBehavior::on_projectile_hit(world, hit.block_pos, projectile);
126 }
127}
128
129#[block_behavior]
131pub struct SulfurSpikeBlock {
132 block: BlockRef,
133}
134
135impl SulfurSpikeBlock {
136 #[must_use]
138 pub const fn new(block: BlockRef) -> Self {
139 Self { block }
140 }
141
142 const fn speleothem(&self) -> SpeleothemBlockBehavior {
143 SpeleothemBlockBehavior {
144 block: self.block,
145 kind: SpeleothemKind::Sulfur,
146 }
147 }
148}
149
150impl BlockBehavior for SulfurSpikeBlock {
151 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
152 self.speleothem().can_survive(state, world, pos)
153 }
154
155 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
156 self.speleothem().state_for_placement(context)
157 }
158
159 fn update_shape(
160 &self,
161 state: BlockStateId,
162 world: &dyn ScheduledTickAccess,
163 pos: BlockPos,
164 direction: Direction,
165 neighbor_pos: BlockPos,
166 neighbor_state: BlockStateId,
167 ) -> BlockStateId {
168 self.speleothem()
169 .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
170 }
171
172 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
173 self.speleothem().tick(state, world, pos);
174 }
175
176 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
177 self.speleothem().random_tick(state, world, pos);
178 }
179
180 fn on_projectile_hit(
181 &self,
182 _state: BlockStateId,
183 world: &Arc<World>,
184 hit: &ClipHitResult,
185 projectile: &dyn Projectile,
186 ) {
187 SpeleothemBlockBehavior::on_projectile_hit(world, hit.block_pos, projectile);
188 }
189}
190
191struct SpeleothemBlockBehavior {
192 block: BlockRef,
193 kind: SpeleothemKind,
194}
195
196#[derive(Clone, Copy)]
197enum SpeleothemKind {
198 PointedDripstone,
199 Sulfur,
200}
201
202const GROWTH_PROBABILITY_PER_RANDOM_TICK: f32 = 0.011_377_778;
203const MAX_GROWTH_LENGTH: i32 = 7;
204const MAX_STALAGMITE_SEARCH_RANGE_WHEN_GROWING: i32 = 10;
205const WATER_TRANSFER_PROBABILITY_PER_RANDOM_TICK: f32 = 45.0 / 256.0;
206const LAVA_TRANSFER_PROBABILITY_PER_RANDOM_TICK: f32 = 15.0 / 256.0;
207const MAX_SEARCH_LENGTH_WHEN_CHECKING_DRIP_TYPE: i32 = 11;
208const MAX_SEARCH_LENGTH_BETWEEN_STALACTITE_TIP_AND_CAULDRON: i32 = 11;
209const DRIP_THROUGH_COLUMN_BOXES: &[BlockLocalAabb] =
210 &[BlockLocalAabb::new(0.375, 0.0, 0.375, 0.625, 1.0, 0.625)];
211const REQUIRED_SPACE_TO_DRIP_THROUGH_NON_SOLID_BLOCK: VoxelShape =
212 VoxelShape::from_boxes(DRIP_THROUGH_COLUMN_BOXES);
213
214struct FluidInfo {
215 pos: BlockPos,
216 fluid: FluidRef,
217 source_state: BlockStateId,
218}
219
220impl SpeleothemBlockBehavior {
221 fn projectile_can_break(projectile: &dyn Projectile, world: &World, pos: BlockPos) -> bool {
222 projectile.projectile_may_interact(world, pos)
223 && projectile.may_break(world)
224 && projectile.entity_type() == &vanilla_entities::TRIDENT
225 && projectile.velocity().length() > 0.6
226 }
227
228 fn on_projectile_hit(world: &Arc<World>, pos: BlockPos, projectile: &dyn Projectile) {
229 if Self::projectile_can_break(projectile, world, pos) {
230 world.destroy_block(pos, true);
231 }
232 }
233
234 fn state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
235 let default_tip_direction = context.get_nearest_looking_vertical_direction().opposite();
236 let tip_direction = self.calculate_tip_direction(
237 context.world.as_ref(),
238 context.place_pos(),
239 default_tip_direction,
240 )?;
241 let merge_opposing_tips = !context.is_secondary_use_active();
242 let thickness = self.calculate_thickness(
243 context.world.as_ref(),
244 context.place_pos(),
245 tip_direction,
246 merge_opposing_tips,
247 );
248 let state = self
249 .block
250 .default_state()
251 .set_value(VERTICAL_DIRECTION, tip_direction)
252 .set_value(WATERLOGGED, context.is_water_source());
253
254 Some(Self::with_thickness(state, thickness))
255 }
256
257 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
258 let tip_direction = state.get_value(VERTICAL_DIRECTION);
259 let behind_pos = pos.relative(tip_direction.opposite());
260 let behind_state = world.get_block_state(behind_pos);
261
262 world.is_face_sturdy(behind_state, behind_pos, tip_direction)
263 || (Self::is_speleothem_with_direction(behind_state, tip_direction)
264 && behind_state.get_block() == self.block)
265 }
266
267 fn update_shape(
268 &self,
269 state: BlockStateId,
270 world: &dyn ScheduledTickAccess,
271 pos: BlockPos,
272 direction: Direction,
273 _neighbor_pos: BlockPos,
274 _neighbor_state: BlockStateId,
275 ) -> BlockStateId {
276 schedule_water_tick_if_waterlogged(state, world, pos);
277
278 if direction != Direction::Up && direction != Direction::Down {
279 return state;
280 }
281
282 let tip_direction = state.get_value(VERTICAL_DIRECTION);
283 if tip_direction == Direction::Down && world.has_scheduled_block_tick(pos, self.block) {
284 return state;
285 }
286
287 if direction == tip_direction.opposite() && !self.can_survive(state, world, pos) {
288 let delay = if tip_direction == Direction::Down {
289 2
290 } else {
291 1
292 };
293 let _ = world.schedule_block_tick_default(pos, self.block, delay);
294 return state;
295 }
296
297 let merge_opposing_tips = Self::thickness(state) == SpeleothemThickness::TipMerge;
298 let thickness = self.calculate_thickness(world, pos, tip_direction, merge_opposing_tips);
299 Self::with_thickness(state, thickness)
300 }
301
302 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
303 if Self::is_stalagmite(state) && !self.can_survive(state, world.as_ref(), pos) {
304 world.destroy_block(pos, true);
305 } else {
306 Self::spawn_falling_stalactite(state, world, pos);
307 }
308 }
309
310 pub fn spawn_falling_stalactite(state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
315 let mut fall_pos = pos;
316 let mut fall_state = state;
317
318 while Self::is_stalactite(fall_state) {
319 let entity = FallingBlockEntity::fall(world, fall_pos, fall_state);
320 if Self::is_tip(fall_state, true) {
321 let size = (1 + pos.y() - fall_pos.y()).max(6);
322 let damage_per_fall_distance = size as f32;
323 entity.set_hurts_entities(damage_per_fall_distance, 40);
324 break;
325 }
326 fall_pos = fall_pos.relative(Direction::Down);
327 fall_state = world.get_block_state(fall_pos);
328 }
329 }
330
331 fn calculate_tip_direction(
332 &self,
333 world: &dyn LevelReader,
334 pos: BlockPos,
335 default_tip_direction: Direction,
336 ) -> Option<Direction> {
337 let default_state = self
338 .block
339 .default_state()
340 .set_value(VERTICAL_DIRECTION, default_tip_direction);
341 if self.can_survive(default_state, world, pos) {
342 return Some(default_tip_direction);
343 }
344
345 let opposite_tip_direction = default_tip_direction.opposite();
346 let opposite_state = self
347 .block
348 .default_state()
349 .set_value(VERTICAL_DIRECTION, opposite_tip_direction);
350 self.can_survive(opposite_state, world, pos)
351 .then_some(opposite_tip_direction)
352 }
353
354 fn calculate_thickness(
355 &self,
356 world: &dyn LevelReader,
357 pos: BlockPos,
358 tip_direction: Direction,
359 merge_opposing_tips: bool,
360 ) -> SpeleothemThickness {
361 let base_direction = tip_direction.opposite();
362 let in_front_state = world.get_block_state(pos.relative(tip_direction));
363 if Self::is_speleothem_with_direction(in_front_state, base_direction)
364 && in_front_state.get_block() == self.block
365 {
366 if merge_opposing_tips
367 || Self::thickness(in_front_state) == SpeleothemThickness::TipMerge
368 {
369 return SpeleothemThickness::TipMerge;
370 }
371 return SpeleothemThickness::Tip;
372 }
373
374 if !Self::is_speleothem_with_direction(in_front_state, tip_direction) {
375 return SpeleothemThickness::Tip;
376 }
377
378 let in_front_thickness = Self::thickness(in_front_state);
379 if matches!(
380 in_front_thickness,
381 SpeleothemThickness::Tip | SpeleothemThickness::TipMerge
382 ) {
383 return SpeleothemThickness::Frustum;
384 }
385
386 let behind_state = world.get_block_state(pos.relative(base_direction));
387 if !Self::is_speleothem_with_direction(behind_state, tip_direction) {
388 return SpeleothemThickness::Base;
389 }
390 SpeleothemThickness::Middle
391 }
392
393 fn is_speleothem_with_direction(state: BlockStateId, tip_direction: Direction) -> bool {
394 state.get_block().has_tag(&BlockTag::SPELEOTHEMS)
395 && state.get_value(VERTICAL_DIRECTION) == tip_direction
396 }
397
398 fn is_stalagmite(state: BlockStateId) -> bool {
399 Self::is_speleothem_with_direction(state, Direction::Up)
400 }
401
402 fn is_stalactite(state: BlockStateId) -> bool {
403 Self::is_speleothem_with_direction(state, Direction::Down)
404 }
405
406 fn is_stalactite_start_pos(
407 &self,
408 state: BlockStateId,
409 world: &dyn LevelReader,
410 pos: BlockPos,
411 ) -> bool {
412 Self::is_stalactite(state) && world.get_block_state(pos.above()).get_block() != self.block
413 }
414
415 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
416 let mut rng = rand::rng();
417 if matches!(self.kind, SpeleothemKind::PointedDripstone) {
418 let random_value = rng.random::<f32>();
419 if (random_value <= WATER_TRANSFER_PROBABILITY_PER_RANDOM_TICK
420 || random_value <= LAVA_TRANSFER_PROBABILITY_PER_RANDOM_TICK)
421 && self.is_stalactite_start_pos(state, world.as_ref(), pos)
422 {
423 self.maybe_transfer_fluid(state, world, pos, random_value);
424 }
425 }
426
427 if rng.random::<f32>() < GROWTH_PROBABILITY_PER_RANDOM_TICK
428 && self.is_stalactite_start_pos(state, world.as_ref(), pos)
429 {
430 self.grow_stalactite_or_stalagmite_if_possible(state, world, pos, &mut rng);
431 }
432 }
433
434 fn grow_stalactite_or_stalagmite_if_possible<R: RngExt + ?Sized>(
435 &self,
436 stalactite_start_state: BlockStateId,
437 world: &Arc<World>,
438 stalactite_start_pos: BlockPos,
439 rng: &mut R,
440 ) {
441 if !self.can_grow(world.as_ref(), stalactite_start_pos) {
442 return;
443 }
444
445 let Some(stalactite_tip_pos) = self.find_tip(
446 stalactite_start_state,
447 world.as_ref(),
448 stalactite_start_pos,
449 MAX_GROWTH_LENGTH,
450 false,
451 ) else {
452 return;
453 };
454
455 let stalactite_tip_state = world.get_block_state(stalactite_tip_pos);
456 if !Self::is_free_hanging_stalactite(stalactite_tip_state)
457 || !self.can_tip_grow(stalactite_tip_state, world, stalactite_tip_pos)
458 {
459 return;
460 }
461
462 if rng.random::<bool>() {
463 self.grow(world, stalactite_tip_pos, Direction::Down);
464 } else {
465 self.grow_stalagmite_below(world, stalactite_tip_pos);
466 }
467 }
468
469 fn can_grow(&self, world: &dyn LevelReader, pos: BlockPos) -> bool {
470 if world.get_block_state(pos.above()).get_block() != self.block_to_grow_on() {
471 return false;
472 }
473
474 if !matches!(self.kind, SpeleothemKind::PointedDripstone) {
475 return true;
476 }
477
478 let fluid_state = world.get_block_state(pos.above_n(2)).get_fluid_state();
479 fluid_state.is_water() && fluid_state.is_source()
480 }
481
482 fn block_to_grow_on(&self) -> BlockRef {
483 match self.kind {
484 SpeleothemKind::PointedDripstone => &vanilla_blocks::DRIPSTONE_BLOCK,
485 SpeleothemKind::Sulfur => &vanilla_blocks::SULFUR,
486 }
487 }
488
489 fn find_tip(
490 &self,
491 speleothem_state: BlockStateId,
492 world: &dyn LevelReader,
493 speleothem_pos: BlockPos,
494 max_search_length: i32,
495 include_merged_tip: bool,
496 ) -> Option<BlockPos> {
497 if Self::is_tip(speleothem_state, include_merged_tip) {
498 return Some(speleothem_pos);
499 }
500
501 let search_direction = speleothem_state.get_value(VERTICAL_DIRECTION);
502 let mut current_pos = speleothem_pos;
503 for _ in 1..max_search_length {
504 current_pos = current_pos.relative(search_direction);
505 let state = world.get_block_state(current_pos);
506 if Self::is_tip(state, include_merged_tip) {
507 return Some(current_pos);
508 }
509
510 if world.is_outside_build_height(current_pos.y())
511 || state.get_block() != self.block
512 || state.get_value(VERTICAL_DIRECTION) != search_direction
513 {
514 return None;
515 }
516 }
517
518 None
519 }
520
521 fn is_tip(state: BlockStateId, include_merged_tip: bool) -> bool {
522 if !state.get_block().has_tag(&BlockTag::SPELEOTHEMS) {
523 return false;
524 }
525
526 let thickness = Self::thickness(state);
527 thickness == SpeleothemThickness::Tip
528 || (include_merged_tip && thickness == SpeleothemThickness::TipMerge)
529 }
530
531 fn is_free_hanging_stalactite(state: BlockStateId) -> bool {
532 Self::is_stalactite(state)
533 && Self::thickness(state) == SpeleothemThickness::Tip
534 && !state.get_value(WATERLOGGED)
535 }
536
537 fn can_tip_grow(&self, tip_state: BlockStateId, world: &Arc<World>, tip_pos: BlockPos) -> bool {
538 let grow_direction = tip_state.get_value(VERTICAL_DIRECTION);
539 let grow_pos = tip_pos.relative(grow_direction);
540 let state_at_grow_pos = world.get_block_state(grow_pos);
541 if state_at_grow_pos.has_fluid() {
542 return false;
543 }
544
545 state_at_grow_pos.is_air()
546 || self.is_unmerged_tip_with_direction(state_at_grow_pos, grow_direction.opposite())
547 }
548
549 fn is_unmerged_tip_with_direction(
550 &self,
551 state: BlockStateId,
552 tip_direction: Direction,
553 ) -> bool {
554 Self::is_tip(state, false)
555 && state.get_block() == self.block
556 && state.get_value(VERTICAL_DIRECTION) == tip_direction
557 }
558
559 fn grow(&self, world: &Arc<World>, grow_from_pos: BlockPos, grow_to_direction: Direction) {
560 let target_pos = grow_from_pos.relative(grow_to_direction);
561 let existing_state_at_target_pos = world.get_block_state(target_pos);
562 if self.is_unmerged_tip_with_direction(
563 existing_state_at_target_pos,
564 grow_to_direction.opposite(),
565 ) {
566 self.create_merged_tips(existing_state_at_target_pos, world, target_pos);
567 return;
568 }
569
570 if existing_state_at_target_pos.is_air()
571 || existing_state_at_target_pos.get_block() == &vanilla_blocks::WATER
572 {
573 self.create_speleothem(
574 world,
575 target_pos,
576 grow_to_direction,
577 SpeleothemThickness::Tip,
578 );
579 }
580 }
581
582 fn create_speleothem(
583 &self,
584 world: &Arc<World>,
585 pos: BlockPos,
586 direction: Direction,
587 thickness: SpeleothemThickness,
588 ) {
589 let waterlogged = world.get_block_state(pos).get_fluid_state().is_water();
590 let state = self
591 .block
592 .default_state()
593 .set_value(VERTICAL_DIRECTION, direction)
594 .set_value(SPELEOTHEM_THICKNESS, thickness)
595 .set_value(WATERLOGGED, waterlogged);
596 world.set_block(pos, state, UpdateFlags::UPDATE_ALL);
597 }
598
599 fn create_merged_tips(&self, tip_state: BlockStateId, world: &Arc<World>, tip_pos: BlockPos) {
600 let (stalactite_pos, stalagmite_pos) =
601 if tip_state.get_value(VERTICAL_DIRECTION) == Direction::Up {
602 (tip_pos.above(), tip_pos)
603 } else {
604 (tip_pos, tip_pos.below())
605 };
606
607 self.create_speleothem(
608 world,
609 stalactite_pos,
610 Direction::Down,
611 SpeleothemThickness::TipMerge,
612 );
613 self.create_speleothem(
614 world,
615 stalagmite_pos,
616 Direction::Up,
617 SpeleothemThickness::TipMerge,
618 );
619 }
620
621 fn grow_stalagmite_below(&self, world: &Arc<World>, pos_above_stalagmite: BlockPos) {
622 let mut pos = pos_above_stalagmite;
623 for _ in 0..MAX_STALAGMITE_SEARCH_RANGE_WHEN_GROWING {
624 pos = pos.below();
625 let state = world.get_block_state(pos);
626 if state.has_fluid() {
627 return;
628 }
629
630 if self.is_unmerged_tip_with_direction(state, Direction::Up)
631 && self.can_tip_grow(state, world, pos)
632 {
633 self.grow(world, pos, Direction::Up);
634 return;
635 }
636
637 let placement_state = self
638 .block
639 .default_state()
640 .set_value(VERTICAL_DIRECTION, Direction::Up);
641 if self.can_survive(placement_state, world.as_ref(), pos)
642 && !Self::is_water_at(world, pos.below())
643 {
644 self.grow(world, pos.below(), Direction::Up);
645 return;
646 }
647
648 if self.blocks_stalagmite_scan(world.as_ref(), pos, state) {
649 return;
650 }
651 }
652 }
653
654 fn is_water_at(world: &Arc<World>, pos: BlockPos) -> bool {
655 world.get_block_state(pos).get_fluid_state().is_water()
656 }
657
658 fn blocks_stalagmite_scan(
659 &self,
660 world: &dyn LevelReader,
661 pos: BlockPos,
662 state: BlockStateId,
663 ) -> bool {
664 match self.kind {
665 SpeleothemKind::PointedDripstone => !Self::can_drip_through(world, pos, state),
666 SpeleothemKind::Sulfur => false,
667 }
668 }
669
670 fn can_drip_through(world: &dyn LevelReader, pos: BlockPos, state: BlockStateId) -> bool {
671 if state.is_air() {
672 return true;
673 }
674
675 if state.is_solid_render() || state.has_fluid() {
676 return false;
677 }
678
679 let collision_shape = BLOCK_BEHAVIORS
680 .get_behavior(state.get_block())
681 .get_collision_shape(state, world, pos, BlockCollisionContext::empty());
682 !join_is_not_empty(
683 REQUIRED_SPACE_TO_DRIP_THROUGH_NON_SOLID_BLOCK,
684 collision_shape,
685 BooleanOp::And,
686 )
687 }
688
689 fn maybe_transfer_fluid(
690 &self,
691 state: BlockStateId,
692 world: &Arc<World>,
693 pos: BlockPos,
694 random_value: f32,
695 ) {
696 let Some(fluid_info) = self.get_fluid_above_stalactite(world, pos, state) else {
697 return;
698 };
699
700 let is_water = fluid_info.fluid == &vanilla_fluids::WATER;
701 let is_lava = fluid_info.fluid == &vanilla_fluids::LAVA;
702
703 let transfer_probability = if is_water {
704 WATER_TRANSFER_PROBABILITY_PER_RANDOM_TICK
705 } else if is_lava {
706 LAVA_TRANSFER_PROBABILITY_PER_RANDOM_TICK
707 } else {
708 return;
709 };
710
711 if random_value >= transfer_probability {
712 return;
713 }
714
715 let Some(tip_pos) = self.find_tip(
716 state,
717 world.as_ref(),
718 pos,
719 MAX_SEARCH_LENGTH_WHEN_CHECKING_DRIP_TYPE,
720 false,
721 ) else {
722 return;
723 };
724
725 if fluid_info.source_state.get_block() == &vanilla_blocks::MUD && is_water {
726 if !world.dimension_type.water_evaporates {
727 let clay_state = vanilla_blocks::CLAY.default_state();
728 let _ =
729 push_entities_up(fluid_info.source_state, clay_state, world, fluid_info.pos);
730 world.set_block(fluid_info.pos, clay_state, UpdateFlags::UPDATE_ALL);
731 world.game_event(
732 &vanilla_game_events::BLOCK_CHANGE,
733 fluid_info.pos,
734 &GameEventContext::new(None, Some(clay_state)),
735 );
736 world.level_event(level_events::DRIPSTONE_DRIP, tip_pos, 0, None);
737 }
738 return;
739 }
740
741 let Some(cauldron_pos) =
742 Self::find_fillable_cauldron_below_stalactite_tip(world, tip_pos, fluid_info.fluid)
743 else {
744 return;
745 };
746
747 world.level_event(level_events::DRIPSTONE_DRIP, tip_pos, 0, None);
748 let fall_distance = tip_pos.y() - cauldron_pos.y();
749 let delay = 50 + fall_distance;
750 let cauldron_state = world.get_block_state(cauldron_pos);
751 let _ = world.schedule_block_tick_default(cauldron_pos, cauldron_state.get_block(), delay);
752 }
753
754 fn get_fluid_above_stalactite(
755 &self,
756 world: &Arc<World>,
757 stalactite_pos: BlockPos,
758 stalactite_state: BlockStateId,
759 ) -> Option<FluidInfo> {
760 if !Self::is_stalactite(stalactite_state) {
761 return None;
762 }
763
764 let root_pos = self.find_root_block(
765 world,
766 stalactite_pos,
767 stalactite_state,
768 MAX_SEARCH_LENGTH_WHEN_CHECKING_DRIP_TYPE,
769 )?;
770 let above_pos = root_pos.above();
771 let above_state = world.get_block_state(above_pos);
772
773 let fluid = if above_state.get_block() == &vanilla_blocks::MUD
774 && !world.dimension_type.water_evaporates
775 {
776 &vanilla_fluids::WATER
777 } else {
778 above_state.get_fluid_state().fluid_id
779 };
780
781 Some(FluidInfo {
782 pos: above_pos,
783 fluid,
784 source_state: above_state,
785 })
786 }
787
788 fn find_root_block(
789 &self,
790 world: &Arc<World>,
791 pos: BlockPos,
792 dripstone_state: BlockStateId,
793 max_search_length: i32,
794 ) -> Option<BlockPos> {
795 let tip_direction = dripstone_state.get_value(VERTICAL_DIRECTION);
796 let search_direction = tip_direction.opposite();
797 let mut current_pos = pos;
798 for _ in 1..max_search_length {
799 current_pos = current_pos.relative(search_direction);
800 let state = world.get_block_state(current_pos);
801 if state.get_block() != self.block {
802 return Some(current_pos);
803 }
804 if state.get_value(VERTICAL_DIRECTION) != tip_direction {
805 return None;
806 }
807 if world.is_outside_build_height(current_pos.y()) {
808 return None;
809 }
810 }
811 None
812 }
813
814 fn find_fillable_cauldron_below_stalactite_tip(
815 world: &Arc<World>,
816 tip_pos: BlockPos,
817 fluid: FluidRef,
818 ) -> Option<BlockPos> {
819 let mut current_pos = tip_pos;
820 for _ in 1..MAX_SEARCH_LENGTH_BETWEEN_STALACTITE_TIP_AND_CAULDRON {
821 current_pos = current_pos.below();
822 let state = world.get_block_state(current_pos);
823
824 if Self::is_fillable_cauldron(state, fluid) {
825 return Some(current_pos);
826 }
827
828 if !Self::can_drip_through(world.as_ref(), current_pos, state) {
829 return None;
830 }
831
832 if world.is_outside_build_height(current_pos.y()) {
833 return None;
834 }
835 }
836 None
837 }
838
839 fn is_fillable_cauldron(state: BlockStateId, fluid: FluidRef) -> bool {
840 let block = state.get_block();
841 if fluid == &vanilla_fluids::WATER {
842 block == &vanilla_blocks::CAULDRON
843 || (block == &vanilla_blocks::WATER_CAULDRON
844 && state.get_value(LEVEL_CAULDRON) < LEVEL_CAULDRON.max)
845 } else if fluid == &vanilla_fluids::LAVA {
846 block == &vanilla_blocks::CAULDRON
847 } else {
848 false
849 }
850 }
851
852 fn thickness(state: BlockStateId) -> SpeleothemThickness {
853 state.get_value(SPELEOTHEM_THICKNESS)
854 }
855
856 fn with_thickness(state: BlockStateId, thickness: SpeleothemThickness) -> BlockStateId {
857 state.set_value(SPELEOTHEM_THICKNESS, thickness)
858 }
859}
860
861#[must_use]
863pub fn find_stalactite_tip_above_cauldron(
864 world: &dyn LevelReader,
865 cauldron_pos: BlockPos,
866) -> Option<BlockPos> {
867 let mut current_pos = cauldron_pos;
868 for _ in 1..MAX_SEARCH_LENGTH_BETWEEN_STALACTITE_TIP_AND_CAULDRON {
869 current_pos = current_pos.above();
870 let state = world.get_block_state(current_pos);
871
872 if SpeleothemBlockBehavior::is_free_hanging_stalactite(state) {
873 return Some(current_pos);
874 }
875
876 if !SpeleothemBlockBehavior::can_drip_through(world, current_pos, state) {
877 return None;
878 }
879
880 if world.is_outside_build_height(current_pos.y()) {
881 return None;
882 }
883 }
884 None
885}
886
887#[must_use]
889pub fn get_cauldron_fill_fluid_type(
890 world: &Arc<World>,
891 stalactite_pos: BlockPos,
892) -> Option<FluidRef> {
893 let state = world.get_block_state(stalactite_pos);
894 let dripstone = SpeleothemBlockBehavior {
895 block: &vanilla_blocks::POINTED_DRIPSTONE,
896 kind: SpeleothemKind::PointedDripstone,
897 };
898 let fluid = dripstone
899 .get_fluid_above_stalactite(world, stalactite_pos, state)?
900 .fluid;
901
902 if fluid == &vanilla_fluids::WATER || fluid == &vanilla_fluids::LAVA {
903 Some(fluid)
904 } else {
905 None
906 }
907}
908
909#[cfg(test)]
910mod tests {
911 use crate::test_support::advance_test_game_time_to;
912 use std::sync::Weak;
913
914 use super::*;
915 use glam::DVec3;
916 use steel_utils::ChunkPos;
917
918 use steel_registry::{
919 entity_type::EntityTypeRef, init_vanilla_registry, vanilla_blocks, vanilla_damage_types,
920 };
921
922 use crate::{
923 behavior::init_behaviors,
924 entity::{Entity, EntityBase, projectile::ProjectileBase},
925 test_support::{fresh_test_world, insert_ready_full_chunk, test_world},
926 };
927
928 struct TestProjectile {
929 base: EntityBase,
930 projectile_base: ProjectileBase,
931 entity_type: EntityTypeRef,
932 }
933
934 impl TestProjectile {
935 fn new(entity_type: EntityTypeRef, velocity: DVec3) -> Self {
936 let projectile = Self {
937 base: EntityBase::new(1, DVec3::ZERO, entity_type.dimensions, Weak::new()),
938 projectile_base: ProjectileBase::new(),
939 entity_type,
940 };
941 projectile.set_velocity(velocity);
942 projectile
943 }
944 }
945
946 crate::entity::impl_test_downcast_type!(TestProjectile);
947
948 impl Entity for TestProjectile {
949 fn base(&self) -> &EntityBase {
950 &self.base
951 }
952
953 fn entity_type(&self) -> EntityTypeRef {
954 self.entity_type
955 }
956 }
957
958 impl Projectile for TestProjectile {
959 fn projectile_base(&self) -> &ProjectileBase {
960 &self.projectile_base
961 }
962 }
963
964 fn pointed_dripstone_state(
965 direction: Direction,
966 thickness: SpeleothemThickness,
967 ) -> BlockStateId {
968 init_vanilla_registry();
969 vanilla_blocks::POINTED_DRIPSTONE
970 .default_state()
971 .set_value(VERTICAL_DIRECTION, direction)
972 .set_value(SPELEOTHEM_THICKNESS, thickness)
973 }
974
975 #[test]
976 fn upward_tip_uses_stalagmite_fall_damage() {
977 let state = pointed_dripstone_state(Direction::Up, SpeleothemThickness::Tip);
978 let fall_damage = PointedDripstoneBlock::fall_damage_for_state(state, 4.0)
979 .expect("upward tip should request stalagmite damage");
980
981 assert!((fall_damage.fall_distance - 6.5).abs() < f64::EPSILON);
982 assert!((fall_damage.damage_modifier - 2.0).abs() < f32::EPSILON);
983 assert_eq!(
984 &fall_damage.source.damage_type.key,
985 &vanilla_damage_types::STALAGMITE.key,
986 );
987 }
988
989 #[test]
990 fn non_tip_uses_default_fall_damage() {
991 let state = pointed_dripstone_state(Direction::Up, SpeleothemThickness::Frustum);
992
993 assert!(PointedDripstoneBlock::fall_damage_for_state(state, 4.0).is_none());
994 }
995
996 #[test]
997 fn downward_tip_uses_default_fall_damage() {
998 let state = pointed_dripstone_state(Direction::Down, SpeleothemThickness::Tip);
999
1000 assert!(PointedDripstoneBlock::fall_damage_for_state(state, 4.0).is_none());
1001 }
1002
1003 #[test]
1004 fn only_fast_tridents_break_speleothems() {
1005 init_vanilla_registry();
1006 let pos = BlockPos::ZERO;
1007 let fast_trident = TestProjectile::new(&vanilla_entities::TRIDENT, DVec3::X * 0.61);
1008 let threshold_trident = TestProjectile::new(&vanilla_entities::TRIDENT, DVec3::X * 0.6);
1009 let firework = TestProjectile::new(&vanilla_entities::FIREWORK_ROCKET, DVec3::X);
1010
1011 assert!(SpeleothemBlockBehavior::projectile_can_break(
1012 &fast_trident,
1013 test_world(),
1014 pos,
1015 ));
1016 assert!(!SpeleothemBlockBehavior::projectile_can_break(
1017 &threshold_trident,
1018 test_world(),
1019 pos,
1020 ));
1021 assert!(!SpeleothemBlockBehavior::projectile_can_break(
1022 &firework,
1023 test_world(),
1024 pos,
1025 ));
1026 }
1027
1028 fn stalactite_setup(
1029 world: &Arc<World>,
1030 pos: BlockPos,
1031 ) -> (SpeleothemBlockBehavior, BlockStateId) {
1032 let dripstone = vanilla_blocks::POINTED_DRIPSTONE
1033 .default_state()
1034 .set_value(VERTICAL_DIRECTION, Direction::Down)
1035 .set_value(SPELEOTHEM_THICKNESS, SpeleothemThickness::Tip);
1036 let root = vanilla_blocks::DRIPSTONE_BLOCK.default_state();
1037 assert!(world.set_block(pos, dripstone, UpdateFlags::UPDATE_NONE));
1038 assert!(world.set_block(pos.above(), root, UpdateFlags::UPDATE_NONE));
1039 let behavior = SpeleothemBlockBehavior {
1040 block: &vanilla_blocks::POINTED_DRIPSTONE,
1041 kind: SpeleothemKind::PointedDripstone,
1042 };
1043 (behavior, dripstone)
1044 }
1045
1046 #[test]
1047 fn stalactite_drip_converts_mud_to_clay() {
1048 init_vanilla_registry();
1049 init_behaviors();
1050 let world = fresh_test_world("mud_to_clay");
1051 let pos = BlockPos::new(8, 64, 8);
1052 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
1053
1054 let mud = vanilla_blocks::MUD.default_state();
1055 assert!(world.set_block(pos.above_n(2), mud, UpdateFlags::UPDATE_NONE));
1056
1057 let (behavior, dripstone) = stalactite_setup(&world, pos);
1058 behavior.maybe_transfer_fluid(dripstone, &world, pos, 0.0);
1059
1060 assert_eq!(
1061 world.get_block_state(pos.above_n(2)).get_block(),
1062 &vanilla_blocks::CLAY,
1063 );
1064 }
1065
1066 #[test]
1067 fn stalactite_drip_fills_empty_cauldron_with_water() {
1068 init_vanilla_registry();
1069 init_behaviors();
1070 let world = fresh_test_world("water_cauldron_fill");
1071 let pos = BlockPos::new(8, 64, 8);
1072 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
1073
1074 let source_water = vanilla_blocks::WATER.default_state();
1075 assert!(world.set_block(pos.above_n(2), source_water, UpdateFlags::UPDATE_NONE));
1076 assert!(world.set_block(
1077 pos.below(),
1078 vanilla_blocks::CAULDRON.default_state(),
1079 UpdateFlags::UPDATE_NONE,
1080 ));
1081
1082 let (behavior, dripstone) = stalactite_setup(&world, pos);
1083 behavior.maybe_transfer_fluid(dripstone, &world, pos, 0.0);
1084
1085 advance_test_game_time_to(&world, 51);
1086 world.chunk_map.tick_game(&world, 51, 0, true);
1087
1088 let cauldron_state = world.get_block_state(pos.below());
1089 assert_eq!(cauldron_state.get_block(), &vanilla_blocks::WATER_CAULDRON);
1090 assert_eq!(cauldron_state.get_value(LEVEL_CAULDRON), 1,);
1091 }
1092
1093 #[test]
1094 fn stalactite_drip_increments_layered_water_cauldron() {
1095 init_vanilla_registry();
1096 init_behaviors();
1097 let world = fresh_test_world("water_cauldron_inc");
1098 let pos = BlockPos::new(8, 64, 8);
1099 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
1100
1101 let source_water = vanilla_blocks::WATER.default_state();
1102 assert!(world.set_block(pos.above_n(2), source_water, UpdateFlags::UPDATE_NONE));
1103 let water_cauldron = vanilla_blocks::WATER_CAULDRON
1104 .default_state()
1105 .set_value(LEVEL_CAULDRON, 1);
1106 assert!(world.set_block(pos.below(), water_cauldron, UpdateFlags::UPDATE_NONE));
1107
1108 let (behavior, dripstone) = stalactite_setup(&world, pos);
1109 behavior.maybe_transfer_fluid(dripstone, &world, pos, 0.0);
1110
1111 advance_test_game_time_to(&world, 51);
1112 world.chunk_map.tick_game(&world, 51, 0, true);
1113
1114 let cauldron_state = world.get_block_state(pos.below());
1115 assert_eq!(cauldron_state.get_block(), &vanilla_blocks::WATER_CAULDRON);
1116 assert_eq!(cauldron_state.get_value(LEVEL_CAULDRON), 2,);
1117 }
1118
1119 #[test]
1120 fn stalactite_drip_does_not_overflow_full_water_cauldron() {
1121 init_vanilla_registry();
1122 init_behaviors();
1123 let world = fresh_test_world("water_cauldron_full");
1124 let pos = BlockPos::new(8, 64, 8);
1125 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
1126
1127 let source_water = vanilla_blocks::WATER.default_state();
1128 assert!(world.set_block(pos.above_n(2), source_water, UpdateFlags::UPDATE_NONE));
1129 let full_cauldron = vanilla_blocks::WATER_CAULDRON
1130 .default_state()
1131 .set_value(LEVEL_CAULDRON, 3);
1132 assert!(world.set_block(pos.below(), full_cauldron, UpdateFlags::UPDATE_NONE));
1133
1134 let (behavior, dripstone) = stalactite_setup(&world, pos);
1135 behavior.maybe_transfer_fluid(dripstone, &world, pos, 0.0);
1136
1137 let cauldron_state = world.get_block_state(pos.below());
1138 assert_eq!(cauldron_state.get_block(), &vanilla_blocks::WATER_CAULDRON);
1139 assert_eq!(cauldron_state.get_value(LEVEL_CAULDRON), 3,);
1140 }
1141
1142 #[test]
1143 fn stalactite_drip_fills_empty_cauldron_with_lava() {
1144 init_vanilla_registry();
1145 init_behaviors();
1146 let world = fresh_test_world("lava_cauldron_fill");
1147 let pos = BlockPos::new(8, 64, 8);
1148 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
1149
1150 let source_lava = vanilla_blocks::LAVA.default_state();
1151 assert!(world.set_block(pos.above_n(2), source_lava, UpdateFlags::UPDATE_NONE));
1152 assert!(world.set_block(
1153 pos.below(),
1154 vanilla_blocks::CAULDRON.default_state(),
1155 UpdateFlags::UPDATE_NONE,
1156 ));
1157
1158 let (behavior, dripstone) = stalactite_setup(&world, pos);
1159 behavior.maybe_transfer_fluid(dripstone, &world, pos, 0.0);
1160
1161 advance_test_game_time_to(&world, 51);
1162 world.chunk_map.tick_game(&world, 51, 0, true);
1163
1164 assert_eq!(
1165 world.get_block_state(pos.below()).get_block(),
1166 &vanilla_blocks::LAVA_CAULDRON,
1167 );
1168 }
1169
1170 #[test]
1171 fn stalactite_drip_does_not_fill_lava_cauldron() {
1172 init_vanilla_registry();
1173 init_behaviors();
1174 let world = fresh_test_world("lava_cauldron_skip");
1175 let pos = BlockPos::new(8, 64, 8);
1176 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
1177
1178 let source_lava = vanilla_blocks::LAVA.default_state();
1179 assert!(world.set_block(pos.above_n(2), source_lava, UpdateFlags::UPDATE_NONE));
1180 assert!(world.set_block(
1181 pos.below(),
1182 vanilla_blocks::LAVA_CAULDRON.default_state(),
1183 UpdateFlags::UPDATE_NONE,
1184 ));
1185
1186 let (behavior, dripstone) = stalactite_setup(&world, pos);
1187 behavior.maybe_transfer_fluid(dripstone, &world, pos, 0.0);
1188
1189 assert_eq!(
1190 world.get_block_state(pos.below()).get_block(),
1191 &vanilla_blocks::LAVA_CAULDRON,
1192 );
1193 }
1194
1195 #[test]
1196 fn stalactite_drip_skips_drip_when_random_value_exceeds_water_probability() {
1197 init_vanilla_registry();
1198 init_behaviors();
1199 let world = fresh_test_world("high_random_water");
1200 let pos = BlockPos::new(8, 64, 8);
1201 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
1202
1203 let source_water = vanilla_blocks::WATER.default_state();
1204 assert!(world.set_block(pos.above_n(2), source_water, UpdateFlags::UPDATE_NONE));
1205 assert!(world.set_block(
1206 pos.below(),
1207 vanilla_blocks::CAULDRON.default_state(),
1208 UpdateFlags::UPDATE_NONE,
1209 ));
1210
1211 let (behavior, dripstone) = stalactite_setup(&world, pos);
1212 behavior.maybe_transfer_fluid(
1213 dripstone,
1214 &world,
1215 pos,
1216 WATER_TRANSFER_PROBABILITY_PER_RANDOM_TICK + 0.01,
1217 );
1218
1219 assert_eq!(
1220 world.get_block_state(pos.below()).get_block(),
1221 &vanilla_blocks::CAULDRON,
1222 );
1223 }
1224
1225 #[test]
1226 fn stalactite_drip_skips_drip_when_random_value_exceeds_lava_probability() {
1227 init_vanilla_registry();
1228 init_behaviors();
1229 let world = fresh_test_world("high_random_lava");
1230 let pos = BlockPos::new(8, 64, 8);
1231 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
1232
1233 let source_lava = vanilla_blocks::LAVA.default_state();
1234 assert!(world.set_block(pos.above_n(2), source_lava, UpdateFlags::UPDATE_NONE));
1235 assert!(world.set_block(
1236 pos.below(),
1237 vanilla_blocks::CAULDRON.default_state(),
1238 UpdateFlags::UPDATE_NONE,
1239 ));
1240
1241 let (behavior, dripstone) = stalactite_setup(&world, pos);
1242 behavior.maybe_transfer_fluid(
1243 dripstone,
1244 &world,
1245 pos,
1246 LAVA_TRANSFER_PROBABILITY_PER_RANDOM_TICK + 0.01,
1247 );
1248
1249 assert_eq!(
1250 world.get_block_state(pos.below()).get_block(),
1251 &vanilla_blocks::CAULDRON,
1252 );
1253 }
1254}