Skip to main content

steel_core/behavior/blocks/vegetation/
cactus_block.rs

1//! Cactus block behavior.
2//!
3//! Cactus grows up to 3 blocks tall via random ticks. It requires sand below
4//! and breaks if any solid block or lava is adjacent horizontally.
5
6use std::sync::Arc;
7
8use steel_macros::block_behavior;
9use steel_registry::blocks::BlockRef;
10use steel_registry::blocks::block_state_ext::BlockStateExt;
11use steel_registry::blocks::properties::{BlockStateProperties, Direction};
12use steel_registry::vanilla_block_tags::BlockTag;
13use steel_registry::vanilla_blocks;
14use steel_registry::vanilla_damage_types;
15use steel_registry::vanilla_fluid_tags;
16use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
17
18use crate::behavior::block::BlockBehavior;
19use crate::behavior::context::BlockPlaceContext;
20use crate::entity::ai::path::PathComputationType;
21use crate::entity::damage::DamageSource;
22use crate::entity::{Entity, InsideBlockEffectCollector};
23use crate::world::{LevelReader, ScheduledTickAccess, World};
24
25/// Maximum cactus stack height (vanilla: 3 blocks).
26const MAX_CACTUS_HEIGHT: u32 = 3;
27
28/// Age at which cactus can attempt to grow a flower (vanilla 1.21+).
29const CACTUS_FLOWER_AGE: u8 = 8;
30
31/// Chance for small cactus (< 3 blocks) to spawn flower.
32const FLOWER_CHANCE_SMALL: f64 = 0.1;
33
34/// Chance for tall cactus (>= 3 blocks) to spawn flower.
35const FLOWER_CHANCE_TALL: f64 = 0.25;
36
37/// Behavior for cactus blocks.
38///
39/// Cactus has special requirements:
40/// - Must be placed on sand, red sand, or another cactus
41/// - Cannot have solid blocks adjacent horizontally
42/// - Grows up to 3 blocks tall via random ticks
43/// - Damages entities that touch it (1 HP per tick)
44#[block_behavior]
45pub struct CactusBlock {
46    block: BlockRef,
47}
48
49impl CactusBlock {
50    /// Creates a new cactus block behavior.
51    #[must_use]
52    pub const fn new(block: BlockRef) -> Self {
53        Self { block }
54    }
55}
56
57impl BlockBehavior for CactusBlock {
58    /// Checks if cactus can survive at the given position.
59    ///
60    /// Survival requirements:
61    /// 1. No solid blocks on horizontal neighbors
62    /// 2. No lava on horizontal neighbors
63    /// 3. Block below must be `CACTUS`, `SAND`, or `RED_SAND`
64    /// 4. Block above must not be liquid
65    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
66        // Check horizontal neighbors - no solid blocks or lava
67        for dir in [
68            Direction::North,
69            Direction::South,
70            Direction::East,
71            Direction::West,
72        ] {
73            let neighbor_pos = dir.relative(pos);
74            let neighbor = world.get_block_state(neighbor_pos);
75            if neighbor.is_solid() {
76                return false;
77            }
78
79            let fluid = neighbor.get_fluid_state();
80            if fluid.fluid_id.has_tag(&vanilla_fluid_tags::FluidTag::LAVA) {
81                return false;
82            }
83        }
84
85        // Block below must be CACTUS or SAND variant
86        let below_pos = pos.below();
87        let below = world.get_block_state(below_pos);
88        let below_block = below.get_block();
89
90        let valid_below = below_block == &vanilla_blocks::CACTUS
91            || below_block.has_tag(&BlockTag::SUPPORTS_CACTUS);
92
93        if !valid_below {
94            return false;
95        }
96
97        // Block above must not be liquid
98        let above = world.get_block_state(pos.above());
99
100        if above.has_fluid() {
101            return false;
102        }
103
104        true
105    }
106
107    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
108        let default_state = self.block.default_state();
109        if self.can_survive(default_state, context.world, context.place_pos()) {
110            Some(default_state)
111        } else {
112            None
113        }
114    }
115
116    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
117        if !self.can_survive(state, world, pos) {
118            world.destroy_block(pos, true);
119        }
120    }
121
122    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
123        let above_pos = pos.above();
124
125        if !world.get_block_state(above_pos).is_air() {
126            return;
127        }
128
129        // Count cactus blocks below
130        let mut height = 1u32;
131        let age = state.get_value(&BlockStateProperties::AGE_15);
132
133        while world
134            .get_block_state(pos.offset(0, -(height as i32), 0))
135            .get_block()
136            == &vanilla_blocks::CACTUS
137        {
138            height += 1;
139            if height == MAX_CACTUS_HEIGHT && age == 15 {
140                return;
141            }
142        }
143
144        // At age 8, chance to grow a cactus flower above
145        if age == CACTUS_FLOWER_AGE
146            && self.can_survive(self.block.default_state(), world, above_pos)
147        {
148            let chance = if height >= MAX_CACTUS_HEIGHT {
149                FLOWER_CHANCE_TALL
150            } else {
151                FLOWER_CHANCE_SMALL
152            };
153            if rand::random::<f64>() <= chance {
154                world.set_block(
155                    above_pos,
156                    vanilla_blocks::CACTUS_FLOWER.default_state(),
157                    UpdateFlags::UPDATE_ALL,
158                );
159            }
160        } else if age == 15 && height < MAX_CACTUS_HEIGHT {
161            world.set_block(
162                above_pos,
163                vanilla_blocks::CACTUS.default_state(),
164                UpdateFlags::UPDATE_ALL,
165            );
166            let new_state = state.set_value(&BlockStateProperties::AGE_15, 0);
167            world.set_block(pos, new_state, UpdateFlags::UPDATE_NONE);
168            world.neighbor_changed(above_pos, &vanilla_blocks::CACTUS);
169        }
170
171        if age < 15 {
172            let new_state = state.set_value(&BlockStateProperties::AGE_15, age + 1);
173            world.set_block(pos, new_state, UpdateFlags::UPDATE_NONE);
174        }
175    }
176
177    fn update_shape(
178        &self,
179        state: BlockStateId,
180        world: &dyn ScheduledTickAccess,
181        pos: BlockPos,
182        _direction: Direction,
183        _neighbor_pos: BlockPos,
184        _neighbor_state: BlockStateId,
185    ) -> BlockStateId {
186        // Vanilla: only schedule a tick if the cactus can't survive
187        if !self.can_survive(state, world, pos) {
188            world.schedule_block_tick_default(pos, self.block, 1);
189        }
190        state
191    }
192
193    fn entity_inside(
194        &self,
195        _state: BlockStateId,
196        world: &Arc<World>,
197        _pos: BlockPos,
198        entity: &dyn Entity,
199        _effect_collector: &mut InsideBlockEffectCollector,
200        _is_precise: bool,
201    ) {
202        entity.hurt(
203            world,
204            &DamageSource::environment(&vanilla_damage_types::CACTUS),
205            1.0,
206        );
207    }
208
209    fn is_pathfindable(
210        &self,
211        _state: BlockStateId,
212        _computation_type: PathComputationType,
213    ) -> bool {
214        false
215    }
216}