steel_core/behavior/blocks/vegetation/
sugar_cane.rs1use 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
21const MAX_SUGAR_CANE_HEIGHT: i32 = 3;
23
24#[block_behavior]
26pub struct SugarCaneBlock {
27 block: BlockRef,
28}
29
30const AGE: &IntProperty = &BlockStateProperties::AGE_15;
31
32impl SugarCaneBlock {
33 #[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(), context.world,
46 pos,
47 ) {
48 Some(self.block.default_state())
49 } else {
50 None
51 }
52 }
53
54 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}