steel_core/behavior/blocks/vegetation/
vine_block.rs1use std::sync::Arc;
2use steel_macros::block_behavior;
3use steel_registry::blocks::block_state_ext::BlockStateExt;
4use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty};
5use steel_registry::{vanilla_blocks, vanilla_game_rules};
6use steel_utils::axis::Axis;
7use steel_utils::types::UpdateFlags;
8use steel_utils::{BlockPos, BlockStateId, Direction};
9
10use crate::behavior::block::{BlockBehavior, default_can_be_replaced};
11use crate::behavior::context::BlockPlaceContext;
12use crate::world::{LevelReader, ScheduledTickAccess, World};
13
14use super::{BlockRef, can_attach_to_multiface};
15
16#[block_behavior]
18pub struct VineBlock {
19 block: BlockRef,
20}
21
22impl VineBlock {
23 #[must_use]
25 pub const fn new(block: BlockRef) -> Self {
26 Self { block }
27 }
28
29 fn has_faces(state: BlockStateId) -> bool {
30 Self::count_faces(state) > 0
31 }
32
33 fn count_faces(state: BlockStateId) -> usize {
34 VINE_FACE_DIRECTIONS
35 .into_iter()
36 .filter(|direction| state.get_value(get_property_for_face(*direction)))
37 .count()
38 }
39
40 fn can_support_at_face(
41 &self,
42 world: &dyn LevelReader,
43 pos: BlockPos,
44 direction: Direction,
45 ) -> bool {
46 if direction == Direction::Down {
47 return false;
48 }
49
50 if can_attach_to_multiface(world, pos.relative(direction), direction) {
51 return true;
52 }
53
54 if direction.get_axis() == Axis::Y {
55 return false;
56 }
57
58 let property = get_property_for_face(direction);
59 let above = world.get_block_state(pos.above());
60 above.get_block() == self.block && above.get_value(property)
61 }
62 fn is_acceptable_neighbour(
63 level: &dyn LevelReader,
64 neighbour_pos: BlockPos,
65 direction_to_neighbour: Direction,
66 ) -> bool {
67 can_attach_to_multiface(level, neighbour_pos, direction_to_neighbour)
68 }
69 fn can_spread(&self, world: &Arc<World>, pos: BlockPos) -> bool {
70 let mut max = 5;
71
72 for x in (pos.x() - 4)..=(pos.x() + 4) {
73 for y in (pos.y() - 1)..=(pos.y() + 1) {
74 for z in (pos.z() - 4)..=(pos.z() + 4) {
75 let block_pos = BlockPos::new(x, y, z);
76
77 if world.get_block_state(block_pos).get_block() == self.block {
78 max -= 1;
79
80 if max <= 0 {
81 return false;
82 }
83 }
84 }
85 }
86 }
87
88 true
89 }
90
91 fn copy_random_faces(from: BlockStateId, to: BlockStateId) -> BlockStateId {
92 let mut result = to;
93 for direction in Direction::HORIZONTAL {
94 if rand::random_bool(0.5) {
95 let property_for_face = get_property_for_face(direction);
96 if from.get_value(property_for_face) {
97 result = result.set_value(property_for_face, true);
98 }
99 }
100 }
101
102 result
103 }
104 fn has_horizontal_connection(state: BlockStateId) -> bool {
105 for dir in Direction::HORIZONTAL {
106 let property = get_property_for_face(dir);
107 if state.get_value(property) {
108 return true;
109 }
110 }
111 false
112 }
113 fn updated_state(
114 &self,
115 mut state: BlockStateId,
116 world: &dyn LevelReader,
117 pos: BlockPos,
118 ) -> BlockStateId {
119 let above_pos = pos.above();
120 if state.get_value(&BlockStateProperties::UP) {
121 state = state.set_value(
122 &BlockStateProperties::UP,
123 Self::is_acceptable_neighbour(world, above_pos, Direction::Down),
124 );
125 }
126
127 let mut above_state: Option<BlockStateId> = None;
128 for direction in Direction::HORIZONTAL {
129 let property = get_property_for_face(direction);
130 if !state.get_value(property) {
131 continue;
132 }
133
134 let mut can_support = self.can_support_at_face(world, pos, direction);
135 if !can_support {
136 let above = *above_state.get_or_insert_with(|| world.get_block_state(above_pos));
137 can_support = above.get_block() == self.block && above.get_value(property);
138 }
139
140 state = state.set_value(property, can_support);
141 }
142
143 state
144 }
145 fn try_spread_horizontal(
146 &self,
147 state: BlockStateId,
148 world: &Arc<World>,
149 pos: BlockPos,
150 test_direction: Direction,
151 ) {
152 if !self.can_spread(world, pos) {
153 return;
154 }
155
156 let test_pos = pos.relative(test_direction);
157 let edge_state = world.get_block_state(test_pos);
158
159 if edge_state.is_air() {
160 let cw = test_direction.rotate_y_clockwise();
161 let cocw = test_direction.rotate_y_counter_clockwise();
162 let cw_property = get_property_for_face(cw);
163 let cocw_property = get_property_for_face(cocw);
164 let cw_has_connecting_face = state.get_value(cw_property);
165 let cocw_has_connecting_face = state.get_value(cocw_property);
166 let cw_test_pos = test_pos.relative(cw);
167 let cocw_test_pos = test_pos.relative(cocw);
168
169 if cw_has_connecting_face && Self::is_acceptable_neighbour(world, cw_test_pos, cw) {
170 world.set_block(
171 test_pos,
172 self.block.default_state().set_value(cw_property, true),
173 UpdateFlags::UPDATE_CLIENTS,
174 );
175 } else if cocw_has_connecting_face
176 && Self::is_acceptable_neighbour(world, cocw_test_pos, cocw)
177 {
178 world.set_block(
179 test_pos,
180 self.block.default_state().set_value(cocw_property, true),
181 UpdateFlags::UPDATE_CLIENTS,
182 );
183 } else {
184 let opposite = test_direction.opposite();
185 if cw_has_connecting_face
186 && world.get_block_state(cw_test_pos).is_air()
187 && Self::is_acceptable_neighbour(world, pos.relative(cw), opposite)
188 {
189 world.set_block(
190 cw_test_pos,
191 self.block
192 .default_state()
193 .set_value(get_property_for_face(opposite), true),
194 UpdateFlags::UPDATE_CLIENTS,
195 );
196 } else if cocw_has_connecting_face
197 && world.get_block_state(cocw_test_pos).is_air()
198 && Self::is_acceptable_neighbour(world, pos.relative(cocw), opposite)
199 {
200 world.set_block(
201 cocw_test_pos,
202 self.block
203 .default_state()
204 .set_value(get_property_for_face(opposite), true),
205 UpdateFlags::UPDATE_CLIENTS,
206 );
207 } else if rand::random_range(0.0..1.0) < 0.05
208 && Self::is_acceptable_neighbour(world, test_pos.above(), Direction::Up)
209 {
210 world.set_block(
211 test_pos,
212 self.block
213 .default_state()
214 .set_value(get_property_for_face(Direction::Up), true),
215 UpdateFlags::UPDATE_CLIENTS,
216 );
217 }
218 }
219 } else if Self::is_acceptable_neighbour(world, test_pos, test_direction) {
220 world.set_block(
221 pos,
222 state.set_value(get_property_for_face(test_direction), true),
223 UpdateFlags::UPDATE_CLIENTS,
224 );
225 }
226 }
227
228 fn try_spread_vertical(
229 &self,
230 state: BlockStateId,
231 world: &Arc<World>,
232 pos: BlockPos,
233 test_direction: Direction,
234 ) {
235 let above_pos = pos.above();
236
237 if test_direction == Direction::Up && pos.y() < world.get_max_y() {
238 if self.can_support_at_face(world, pos, test_direction) {
239 world.set_block(
240 pos,
241 state.set_value(get_property_for_face(Direction::Up), true),
242 UpdateFlags::UPDATE_CLIENTS,
243 );
244 return;
245 }
246 if world.get_block_state(above_pos).is_air() {
247 if !self.can_spread(world, pos) {
248 return;
249 }
250 let mut above_state = state;
251 for direction in Direction::HORIZONTAL {
252 if rand::random_bool(0.5)
253 || !Self::is_acceptable_neighbour(
254 world,
255 above_pos.relative(direction),
256 direction,
257 )
258 {
259 above_state =
260 above_state.set_value(get_property_for_face(direction), false);
261 }
262 }
263 if Self::has_horizontal_connection(above_state) {
264 world.set_block(above_pos, above_state, UpdateFlags::UPDATE_CLIENTS);
265 }
266 return;
267 }
268 }
269
270 if pos.y() > world.min_y() {
271 let below_pos = pos.below();
272 let below_state = world.get_block_state(below_pos);
273 if below_state.is_air() || below_state.get_block() == self.block {
274 let before = if below_state.is_air() {
275 self.block.default_state()
276 } else {
277 below_state
278 };
279 let after = Self::copy_random_faces(state, before);
280 if before != after && Self::has_horizontal_connection(after) {
281 world.set_block(below_pos, after, UpdateFlags::UPDATE_CLIENTS);
282 }
283 }
284 }
285 }
286}
287
288const VINE_FACE_DIRECTIONS: [Direction; 5] = [
289 Direction::Up,
290 Direction::North,
291 Direction::East,
292 Direction::South,
293 Direction::West,
294];
295
296fn get_property_for_face(direction: Direction) -> &'static BoolProperty {
297 match direction {
298 Direction::Up => &BlockStateProperties::UP,
299 Direction::North => &BlockStateProperties::NORTH,
300 Direction::East => &BlockStateProperties::EAST,
301 Direction::South => &BlockStateProperties::SOUTH,
302 Direction::West => &BlockStateProperties::WEST,
303 Direction::Down => unreachable!("vine has no DOWN face property"),
304 }
305}
306
307impl BlockBehavior for VineBlock {
308 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
309 Self::has_faces(self.updated_state(state, world, pos))
310 }
311
312 fn update_shape(
313 &self,
314 state: BlockStateId,
315 world: &dyn ScheduledTickAccess,
316 pos: BlockPos,
317 direction: Direction,
318 _neighbor_pos: BlockPos,
319 _neighbor_state: BlockStateId,
320 ) -> BlockStateId {
321 if direction == Direction::Down {
322 return state;
323 }
324
325 let updated = self.updated_state(state, world, pos);
326 if Self::has_faces(updated) {
327 updated
328 } else {
329 vanilla_blocks::AIR.default_state()
330 }
331 }
332
333 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
334 if !world.get_game_rule(&vanilla_game_rules::SPREAD_VINES) {
335 return;
336 }
337 if rand::random_range(0..4) != 0 {
338 return;
339 }
340
341 let test_direction = Direction::random();
342
343 if test_direction.axis().is_horizontal()
344 && !state.get_value(get_property_for_face(test_direction))
345 {
346 self.try_spread_horizontal(state, world, pos, test_direction);
347 } else {
348 self.try_spread_vertical(state, world, pos, test_direction);
349 }
350 }
351
352 fn can_be_replaced(&self, state: BlockStateId, context: &BlockPlaceContext<'_>) -> bool {
353 let clicked_state = context.world.get_block_state(context.place_pos());
354 if clicked_state.get_block() == self.block {
355 Self::count_faces(clicked_state) < VINE_FACE_DIRECTIONS.len()
356 } else {
357 default_can_be_replaced(state, context)
358 }
359 }
360
361 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
362 let clicked_pos = context.place_pos();
363 let clicked_state = context.world.get_block_state(clicked_pos);
364 let clicked_vine = clicked_state.get_block() == self.block;
365 let result = if clicked_vine {
366 clicked_state
367 } else {
368 self.block.default_state()
369 };
370
371 for direction in context.get_nearest_looking_directions() {
372 if direction != Direction::Down {
373 let face = get_property_for_face(direction);
374 let face_occupied = clicked_vine && clicked_state.get_value(face);
375 if !face_occupied && self.can_support_at_face(context.world, clicked_pos, direction)
376 {
377 return Some(result.set_value(face, true));
378 }
379 }
380 }
381 if clicked_vine {
382 return Some(result);
383 }
384 None
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::test_support::TestLevel;
392 use steel_registry::init_vanilla_registry;
393
394 #[test]
395 fn face_count_matches_vanilla_replacement_limit() {
396 init_vanilla_registry();
397
398 let mut state = vanilla_blocks::VINE.default_state();
399 assert_eq!(VineBlock::count_faces(state), 0);
400
401 for (expected, direction) in VINE_FACE_DIRECTIONS.into_iter().enumerate() {
402 state = state.set_value(get_property_for_face(direction), true);
403 assert_eq!(VineBlock::count_faces(state), expected + 1);
404 }
405 }
406
407 #[test]
408 fn shape_update_removes_faceless_vine() {
409 init_vanilla_registry();
410
411 let vine = VineBlock::new(&vanilla_blocks::VINE);
412 let state = vanilla_blocks::VINE.default_state();
413 let level = TestLevel::default();
414
415 assert_eq!(
416 vine.update_shape(
417 state,
418 &level,
419 BlockPos::ZERO,
420 Direction::North,
421 BlockPos::ZERO.north(),
422 vanilla_blocks::AIR.default_state(),
423 ),
424 vanilla_blocks::AIR.default_state()
425 );
426 }
427}