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, IntProperty};
12use steel_registry::vanilla_block_tags::BlockTag;
13use steel_registry::vanilla_blocks;
14use steel_registry::vanilla_damage_types;
15use steel_registry::vanilla_fluid_tags::FluidTag;
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
49const AGE: &IntProperty = &BlockStateProperties::AGE_15;
50
51impl CactusBlock {
52    /// Creates a new cactus block behavior.
53    #[must_use]
54    pub const fn new(block: BlockRef) -> Self {
55        Self { block }
56    }
57}
58
59impl BlockBehavior for CactusBlock {
60    /// Checks if cactus can survive at the given position.
61    ///
62    /// Survival requirements:
63    /// 1. No solid blocks on horizontal neighbors
64    /// 2. No lava on horizontal neighbors
65    /// 3. Block below must be `CACTUS`, `SAND`, or `RED_SAND`
66    /// 4. Block above must not be liquid
67    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
68        // Check horizontal neighbors - no solid blocks or lava
69        for dir in [
70            Direction::North,
71            Direction::South,
72            Direction::East,
73            Direction::West,
74        ] {
75            let neighbor_pos = dir.relative(pos);
76            let neighbor = world.get_block_state(neighbor_pos);
77            if neighbor.is_solid() {
78                return false;
79            }
80
81            let fluid = neighbor.get_fluid_state();
82            if fluid.fluid_id.has_tag(&FluidTag::LAVA) {
83                return false;
84            }
85        }
86
87        // Block below must be CACTUS or SAND variant
88        let below_pos = pos.below();
89        let below = world.get_block_state(below_pos);
90        let below_block = below.get_block();
91
92        let valid_below = below_block == &vanilla_blocks::CACTUS
93            || below_block.has_tag(&BlockTag::SUPPORTS_CACTUS);
94
95        if !valid_below {
96            return false;
97        }
98
99        // Block above must not be liquid
100        let above = world.get_block_state(pos.above());
101
102        !above.has_fluid()
103    }
104
105    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
106        let default_state = self.block.default_state();
107        if self.can_survive(default_state, context.world, context.place_pos()) {
108            Some(default_state)
109        } else {
110            None
111        }
112    }
113
114    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
115        if !self.can_survive(state, world, pos) {
116            world.destroy_block(pos, true);
117        }
118    }
119
120    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
121        let above_pos = pos.above();
122
123        if !world.get_block_state(above_pos).is_air() {
124            return;
125        }
126
127        // Count cactus blocks below
128        let mut height = 1u32;
129        let age = state.get_value(AGE);
130
131        while world
132            .get_block_state(pos.offset(0, -(height as i32), 0))
133            .get_block()
134            == &vanilla_blocks::CACTUS
135        {
136            height += 1;
137            if height == MAX_CACTUS_HEIGHT && age == AGE.max {
138                return;
139            }
140        }
141
142        // At age 8, chance to grow a cactus flower above
143        if age == CACTUS_FLOWER_AGE
144            && self.can_survive(self.block.default_state(), world, above_pos)
145        {
146            let chance = if height >= MAX_CACTUS_HEIGHT {
147                FLOWER_CHANCE_TALL
148            } else {
149                FLOWER_CHANCE_SMALL
150            };
151            if rand::random::<f64>() <= chance {
152                world.set_block(
153                    above_pos,
154                    vanilla_blocks::CACTUS_FLOWER.default_state(),
155                    UpdateFlags::UPDATE_ALL,
156                );
157            }
158        } else if age == AGE.max && height < MAX_CACTUS_HEIGHT {
159            world.set_block(
160                above_pos,
161                vanilla_blocks::CACTUS.default_state(),
162                UpdateFlags::UPDATE_ALL,
163            );
164            let new_state = state.set_value(AGE, 0);
165            world.set_block(pos, new_state, UpdateFlags::UPDATE_NONE);
166            world.neighbor_changed(above_pos, &vanilla_blocks::CACTUS);
167        }
168
169        if age < AGE.max {
170            let new_state = state.set_value(AGE, age + 1);
171            world.set_block(pos, new_state, UpdateFlags::UPDATE_NONE);
172        }
173    }
174
175    fn update_shape(
176        &self,
177        state: BlockStateId,
178        world: &dyn ScheduledTickAccess,
179        pos: BlockPos,
180        _direction: Direction,
181        _neighbor_pos: BlockPos,
182        _neighbor_state: BlockStateId,
183    ) -> BlockStateId {
184        // Vanilla: only schedule a tick if the cactus can't survive
185        if !self.can_survive(state, world, pos) {
186            world.schedule_block_tick_default(pos, self.block, 1);
187        }
188        state
189    }
190
191    fn entity_inside(
192        &self,
193        _state: BlockStateId,
194        world: &Arc<World>,
195        _pos: BlockPos,
196        entity: &dyn Entity,
197        _effect_collector: &mut InsideBlockEffectCollector,
198        _is_precise: bool,
199    ) {
200        entity.hurt(
201            world,
202            &DamageSource::environment(&vanilla_damage_types::CACTUS),
203            1.0,
204        );
205    }
206
207    fn is_pathfindable(
208        &self,
209        _state: BlockStateId,
210        _computation_type: PathComputationType,
211    ) -> bool {
212        false
213    }
214}