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        if above.has_fluid() {
103            return false;
104        }
105
106        true
107    }
108
109    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
110        let default_state = self.block.default_state();
111        if self.can_survive(default_state, context.world, context.place_pos()) {
112            Some(default_state)
113        } else {
114            None
115        }
116    }
117
118    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
119        if !self.can_survive(state, world, pos) {
120            world.destroy_block(pos, true);
121        }
122    }
123
124    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
125        let above_pos = pos.above();
126
127        if !world.get_block_state(above_pos).is_air() {
128            return;
129        }
130
131        // Count cactus blocks below
132        let mut height = 1u32;
133        let age = state.get_value(AGE);
134
135        while world
136            .get_block_state(pos.offset(0, -(height as i32), 0))
137            .get_block()
138            == &vanilla_blocks::CACTUS
139        {
140            height += 1;
141            if height == MAX_CACTUS_HEIGHT && age == AGE.max {
142                return;
143            }
144        }
145
146        // At age 8, chance to grow a cactus flower above
147        if age == CACTUS_FLOWER_AGE
148            && self.can_survive(self.block.default_state(), world, above_pos)
149        {
150            let chance = if height >= MAX_CACTUS_HEIGHT {
151                FLOWER_CHANCE_TALL
152            } else {
153                FLOWER_CHANCE_SMALL
154            };
155            if rand::random::<f64>() <= chance {
156                world.set_block(
157                    above_pos,
158                    vanilla_blocks::CACTUS_FLOWER.default_state(),
159                    UpdateFlags::UPDATE_ALL,
160                );
161            }
162        } else if age == AGE.max && height < MAX_CACTUS_HEIGHT {
163            world.set_block(
164                above_pos,
165                vanilla_blocks::CACTUS.default_state(),
166                UpdateFlags::UPDATE_ALL,
167            );
168            let new_state = state.set_value(AGE, 0);
169            world.set_block(pos, new_state, UpdateFlags::UPDATE_NONE);
170            world.neighbor_changed(above_pos, &vanilla_blocks::CACTUS);
171        }
172
173        if age < AGE.max {
174            let new_state = state.set_value(AGE, age + 1);
175            world.set_block(pos, new_state, UpdateFlags::UPDATE_NONE);
176        }
177    }
178
179    fn update_shape(
180        &self,
181        state: BlockStateId,
182        world: &dyn ScheduledTickAccess,
183        pos: BlockPos,
184        _direction: Direction,
185        _neighbor_pos: BlockPos,
186        _neighbor_state: BlockStateId,
187    ) -> BlockStateId {
188        // Vanilla: only schedule a tick if the cactus can't survive
189        if !self.can_survive(state, world, pos) {
190            world.schedule_block_tick_default(pos, self.block, 1);
191        }
192        state
193    }
194
195    fn entity_inside(
196        &self,
197        _state: BlockStateId,
198        world: &Arc<World>,
199        _pos: BlockPos,
200        entity: &dyn Entity,
201        _effect_collector: &mut InsideBlockEffectCollector,
202        _is_precise: bool,
203    ) {
204        entity.hurt(
205            world,
206            &DamageSource::environment(&vanilla_damage_types::CACTUS),
207            1.0,
208        );
209    }
210
211    fn is_pathfindable(
212        &self,
213        _state: BlockStateId,
214        _computation_type: PathComputationType,
215    ) -> bool {
216        false
217    }
218}