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