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