Skip to main content

steel_core/behavior/blocks/vegetation/
sugar_cane.rs

1//! Sugar Cane block behavior.
2//!
3//! Sugar cane grows up to 3 blocks tall via random ticks. It requires water adjacent
4//! to the block it is planted on (or frosted ice).
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_fluid_tags;
15use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
16
17use crate::behavior::BlockBehavior;
18use crate::behavior::context::BlockPlaceContext;
19use crate::world::{LevelReader, ScheduledTickAccess, World};
20
21/// Maximum sugar cane stack height (vanilla: 3 blocks).
22const MAX_SUGAR_CANE_HEIGHT: i32 = 3;
23
24/// Behavior for sugar cane blocks.
25#[block_behavior]
26pub struct SugarCaneBlock {
27    block: BlockRef,
28}
29
30impl SugarCaneBlock {
31    /// Creates a new sugar cane block behavior.
32    #[must_use]
33    pub const fn new(block: BlockRef) -> Self {
34        Self { block }
35    }
36}
37
38impl BlockBehavior for SugarCaneBlock {
39    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
40        let pos = context.place_pos();
41        if self.can_survive(
42            vanilla_blocks::SUGAR_CANE.default_state(), // state argument is unused
43            context.world,
44            pos,
45        ) {
46            Some(self.block.default_state())
47        } else {
48            None
49        }
50    }
51
52    /// Called when this block is placed.
53    fn on_place(
54        &self,
55        state: BlockStateId,
56        world: &Arc<World>,
57        pos: BlockPos,
58        old_state: BlockStateId,
59        _moved_by_piston: bool,
60    ) {
61        if state.get_block() == old_state.get_block() {
62            return;
63        }
64
65        if !self.can_survive(state, world, pos) {
66            world.schedule_block_tick_default(pos, state.get_block(), 1);
67        }
68    }
69
70    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
71        if !self.can_survive(state, world, pos) {
72            world.destroy_block(pos, true);
73        }
74    }
75
76    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
77        let above_pos = pos.above();
78
79        if !world.get_block_state(above_pos).is_air() {
80            return;
81        }
82
83        let mut height = 1i32;
84        while world.get_block_state(pos.below_n(height)).get_block() == self.block {
85            height += 1;
86        }
87
88        if height >= MAX_SUGAR_CANE_HEIGHT {
89            return;
90        }
91
92        let age = state.get_value(&BlockStateProperties::AGE_15);
93
94        if age == 15 {
95            world.set_block(
96                above_pos,
97                self.block.default_state(),
98                UpdateFlags::UPDATE_ALL,
99            );
100            let new_state = state.set_value(&BlockStateProperties::AGE_15, 0);
101            world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
102        } else {
103            let new_state = state.set_value(&BlockStateProperties::AGE_15, age + 1);
104            world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
105        }
106    }
107
108    fn update_shape(
109        &self,
110        state: BlockStateId,
111        world: &dyn ScheduledTickAccess,
112        pos: BlockPos,
113        _direction: Direction,
114        _neighbor_pos: BlockPos,
115        _neighbor_state: BlockStateId,
116    ) -> BlockStateId {
117        if !self.can_survive(state, world, pos) {
118            world.schedule_block_tick_default(pos, self.block, 1);
119        }
120        state
121    }
122
123    fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
124        let below_pos = pos.below();
125        let below_state = world.get_block_state(below_pos);
126        let below_block = below_state.get_block();
127
128        if below_block == self.block {
129            return true;
130        }
131
132        let is_valid_ground = below_block.has_tag(&BlockTag::SUPPORTS_SUGAR_CANE);
133
134        if !is_valid_ground {
135            return false;
136        }
137
138        for dir in [
139            Direction::North,
140            Direction::South,
141            Direction::East,
142            Direction::West,
143        ] {
144            let neighbor_pos = dir.relative(below_pos);
145            let neighbor_state = world.get_block_state(neighbor_pos);
146
147            if neighbor_state
148                .get_block()
149                .has_tag(&BlockTag::SUPPORTS_SUGAR_CANE_ADJACENTLY)
150                || neighbor_state
151                    .get_fluid_state()
152                    .fluid_id
153                    .has_tag(&vanilla_fluid_tags::FluidTag::SUPPORTS_SUGAR_CANE_ADJACENTLY)
154            {
155                return true;
156            }
157        }
158
159        false
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use steel_registry::init_vanilla_registry;
166
167    use crate::test_support::TestLevel;
168
169    use super::*;
170
171    #[test]
172    fn sugar_cane_update_shape_schedules_break_tick_when_unsupported() {
173        init_vanilla_registry();
174        let behavior = SugarCaneBlock::new(&vanilla_blocks::SUGAR_CANE);
175        let level = TestLevel::default();
176        let state = vanilla_blocks::SUGAR_CANE.default_state();
177
178        let updated = behavior.update_shape(
179            state,
180            &level,
181            BlockPos::ZERO,
182            Direction::Down,
183            BlockPos::ZERO.below(),
184            vanilla_blocks::AIR.default_state(),
185        );
186
187        assert_eq!(updated, state);
188        assert!(
189            level
190                .scheduled_block_ticks
191                .borrow()
192                .iter()
193                .any(|tick| tick.block == &vanilla_blocks::SUGAR_CANE)
194        );
195    }
196}