steel_core/behavior/blocks/vegetation/
chorus_plant_block.rs1use std::sync::Arc;
2
3use steel_macros::block_behavior;
4use steel_registry::blocks::block_state_ext::BlockStateExt;
5use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty};
6use steel_registry::vanilla_block_tags::BlockTag;
7use steel_registry::vanilla_blocks;
8use steel_utils::{BlockPos, BlockStateId, Direction};
9
10use crate::behavior::block::BlockBehavior;
11use crate::behavior::context::BlockPlaceContext;
12use crate::entity::ai::path::PathComputationType;
13use crate::world::{LevelReader, ScheduledTickAccess, World};
14
15use super::BlockRef;
16
17const HORIZONTAL_DIRECTIONS: [Direction; 4] = [
18 Direction::North,
19 Direction::East,
20 Direction::South,
21 Direction::West,
22];
23
24#[block_behavior]
26pub struct ChorusPlantBlock {
27 block: BlockRef,
28}
29
30const DOWN: &BoolProperty = &BlockStateProperties::DOWN;
31const EAST: &BoolProperty = &BlockStateProperties::EAST;
32const NORTH: &BoolProperty = &BlockStateProperties::NORTH;
33const SOUTH: &BoolProperty = &BlockStateProperties::SOUTH;
34const UP: &BoolProperty = &BlockStateProperties::UP;
35const WEST: &BoolProperty = &BlockStateProperties::WEST;
36
37impl ChorusPlantBlock {
38 #[must_use]
40 pub const fn new(block: BlockRef) -> Self {
41 Self { block }
42 }
43
44 #[must_use]
45 pub(crate) fn state_with_connections(
46 world: &dyn LevelReader,
47 pos: BlockPos,
48 mut state: BlockStateId,
49 ) -> BlockStateId {
50 let down = world.get_block_state(pos.below());
51 let up = world.get_block_state(pos.above());
52 let north = world.get_block_state(pos.north());
53 let east = world.get_block_state(pos.east());
54 let south = world.get_block_state(pos.south());
55 let west = world.get_block_state(pos.west());
56 let block = state.get_block();
57
58 state = state.set_value(
59 DOWN,
60 down.get_block() == block
61 || down.get_block() == &vanilla_blocks::CHORUS_FLOWER
62 || down.get_block().has_tag(&BlockTag::SUPPORTS_CHORUS_PLANT),
63 );
64 state = state.set_value(
65 UP,
66 up.get_block() == block || up.get_block() == &vanilla_blocks::CHORUS_FLOWER,
67 );
68 state = state.set_value(
69 NORTH,
70 north.get_block() == block || north.get_block() == &vanilla_blocks::CHORUS_FLOWER,
71 );
72 state = state.set_value(
73 EAST,
74 east.get_block() == block || east.get_block() == &vanilla_blocks::CHORUS_FLOWER,
75 );
76 state = state.set_value(
77 SOUTH,
78 south.get_block() == block || south.get_block() == &vanilla_blocks::CHORUS_FLOWER,
79 );
80 state.set_value(
81 WEST,
82 west.get_block() == block || west.get_block() == &vanilla_blocks::CHORUS_FLOWER,
83 )
84 }
85
86 const fn property_for_direction(direction: Direction) -> &'static BoolProperty {
87 match direction {
88 Direction::Down => DOWN,
89 Direction::Up => UP,
90 Direction::North => NORTH,
91 Direction::South => SOUTH,
92 Direction::West => WEST,
93 Direction::East => EAST,
94 }
95 }
96}
97
98impl BlockBehavior for ChorusPlantBlock {
99 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
100 let below_state = world.get_block_state(pos.below());
101 let block_above_or_below =
102 !world.get_block_state(pos.above()).is_air() && !below_state.is_air();
103
104 for direction in HORIZONTAL_DIRECTIONS {
105 let neighbor_pos = pos.relative(direction);
106 let neighbor_state = world.get_block_state(neighbor_pos);
107 if neighbor_state.get_block() == self.block {
108 if block_above_or_below {
109 return false;
110 }
111
112 let below = world.get_block_state(neighbor_pos.below());
113 if below.get_block() == self.block
114 || below.get_block().has_tag(&BlockTag::SUPPORTS_CHORUS_PLANT)
115 {
116 return true;
117 }
118 }
119 }
120
121 below_state.get_block() == self.block
122 || below_state
123 .get_block()
124 .has_tag(&BlockTag::SUPPORTS_CHORUS_PLANT)
125 }
126
127 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
128 Some(Self::state_with_connections(
129 context.world.as_ref(),
130 context.place_pos(),
131 self.block.default_state(),
132 ))
133 }
134
135 fn update_shape(
136 &self,
137 state: BlockStateId,
138 world: &dyn ScheduledTickAccess,
139 pos: BlockPos,
140 direction: Direction,
141 _neighbor_pos: BlockPos,
142 neighbor_state: BlockStateId,
143 ) -> BlockStateId {
144 if !self.can_survive(state, world, pos) {
145 world.schedule_block_tick_default(pos, self.block, 1);
146 return state;
147 }
148
149 let connects = neighbor_state.get_block() == self.block
150 || neighbor_state.get_block() == &vanilla_blocks::CHORUS_FLOWER
151 || direction == Direction::Down
152 && neighbor_state
153 .get_block()
154 .has_tag(&BlockTag::SUPPORTS_CHORUS_PLANT);
155 state.set_value(Self::property_for_direction(direction), connects)
156 }
157
158 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
159 if !self.can_survive(state, world, pos) {
160 world.destroy_block(pos, true);
161 }
162 }
163
164 fn is_pathfindable(
165 &self,
166 _state: BlockStateId,
167 _computation_type: PathComputationType,
168 ) -> bool {
169 false
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use steel_registry::init_vanilla_registry;
176
177 use crate::test_support::TestLevel;
178
179 use super::*;
180
181 #[test]
182 fn update_shape_tracks_vertical_connections() {
183 init_vanilla_registry();
184 let behavior = ChorusPlantBlock::new(&vanilla_blocks::CHORUS_PLANT);
185 let pos = BlockPos::ZERO;
186 let state = vanilla_blocks::CHORUS_PLANT.default_state();
187 let level =
188 TestLevel::default().with_block(pos.below(), vanilla_blocks::END_STONE.default_state());
189
190 let connected = behavior.update_shape(
191 state,
192 &level,
193 pos,
194 Direction::Up,
195 pos.above(),
196 vanilla_blocks::CHORUS_FLOWER.default_state(),
197 );
198 assert!(connected.get_value(UP));
199
200 let disconnected = behavior.update_shape(
201 connected,
202 &level,
203 pos,
204 Direction::Up,
205 pos.above(),
206 vanilla_blocks::AIR.default_state(),
207 );
208 assert!(!disconnected.get_value(UP));
209 }
210
211 #[test]
212 fn update_shape_schedules_tick_when_unsupported() {
213 init_vanilla_registry();
214 let behavior = ChorusPlantBlock::new(&vanilla_blocks::CHORUS_PLANT);
215 let level = TestLevel::default();
216 let pos = BlockPos::ZERO;
217 let state = vanilla_blocks::CHORUS_PLANT.default_state();
218
219 assert_eq!(
220 behavior.update_shape(
221 state,
222 &level,
223 pos,
224 Direction::Down,
225 pos.below(),
226 vanilla_blocks::AIR.default_state(),
227 ),
228 state
229 );
230
231 let scheduled = level.scheduled_block_ticks.borrow();
232 assert_eq!(scheduled.len(), 1);
233 assert_eq!(scheduled[0].pos, pos);
234 assert_eq!(scheduled[0].block, &vanilla_blocks::CHORUS_PLANT);
235 assert_eq!(scheduled[0].delay, 1);
236 }
237
238 #[test]
239 fn is_never_pathfindable() {
240 init_vanilla_registry();
241 let behavior = ChorusPlantBlock::new(&vanilla_blocks::CHORUS_PLANT);
242 let state = vanilla_blocks::CHORUS_PLANT.default_state();
243
244 assert!(!behavior.is_pathfindable(state, PathComputationType::Land));
245 assert!(!behavior.is_pathfindable(state, PathComputationType::Water));
246 assert!(!behavior.is_pathfindable(state, PathComputationType::Air));
247 }
248}