1use std::sync::Arc;
2
3use rand::{Rng, RngExt};
4use steel_macros::block_behavior;
5use steel_registry::blocks::block_state_ext::BlockStateExt;
6use steel_registry::blocks::properties::{BlockStateProperties, IntProperty};
7use steel_registry::level_events;
8use steel_registry::vanilla_block_tags::BlockTag;
9use steel_utils::{BlockPos, BlockStateId, Direction, types::UpdateFlags};
10
11use crate::behavior::block::BlockBehavior;
12use crate::behavior::context::BlockPlaceContext;
13use crate::entity::projectile::Projectile;
14use crate::world::{ClipHitResult, LevelReader, ScheduledTickAccess, World};
15
16use super::{BlockRef, ChorusPlantBlock, default_surviving_state};
17
18const HORIZONTAL_DIRECTIONS: [Direction; 4] = [
19 Direction::North,
20 Direction::East,
21 Direction::South,
22 Direction::West,
23];
24
25#[block_behavior]
27pub struct ChorusFlowerBlock {
28 block: BlockRef,
29 #[json_arg(vanilla_blocks)]
30 plant: BlockRef,
31}
32
33const AGE: &IntProperty = &BlockStateProperties::AGE_5;
34const DEAD_AGE: u8 = 5;
35const PILLAR_SCAN_DEPTH: i32 = 4;
36const MIN_PILLAR_HEIGHT: i32 = 2;
37const GROWTH_RANDOM_RANGE: i32 = 4;
38const GROWTH_RANDOM_RANGE_ON_SUPPORT: i32 = 5;
39const BRANCH_ATTEMPT_RANDOM_RANGE: i32 = 4;
40
41impl ChorusFlowerBlock {
42 #[must_use]
44 pub const fn new(block: BlockRef, plant: BlockRef) -> Self {
45 Self { block, plant }
46 }
47
48 fn projectile_can_break(projectile: &dyn Projectile, world: &World, pos: BlockPos) -> bool {
49 projectile.projectile_may_interact(world, pos) && projectile.may_break(world)
50 }
51
52 fn all_neighbors_empty(
53 world: &dyn LevelReader,
54 pos: BlockPos,
55 ignore: Option<Direction>,
56 ) -> bool {
57 HORIZONTAL_DIRECTIONS.into_iter().all(|direction| {
58 Some(direction) == ignore || world.get_block_state(pos.relative(direction)).is_air()
59 })
60 }
61
62 fn place_grown_flower(&self, world: &Arc<World>, pos: BlockPos, age: u8) {
63 world.set_block(
64 pos,
65 self.block.default_state().set_value(AGE, age),
66 UpdateFlags::UPDATE_CLIENTS,
67 );
68 world.level_event(level_events::SOUND_CHORUS_GROW, pos, 0, None);
69 }
70
71 fn place_dead_flower(&self, world: &Arc<World>, pos: BlockPos) {
72 world.set_block(
73 pos,
74 self.block.default_state().set_value(AGE, DEAD_AGE),
75 UpdateFlags::UPDATE_CLIENTS,
76 );
77 world.level_event(level_events::SOUND_CHORUS_DEATH, pos, 0, None);
78 }
79
80 fn random_tick_with_rng(
81 &self,
82 state: BlockStateId,
83 world: &Arc<World>,
84 pos: BlockPos,
85 rng: &mut dyn Rng,
86 ) {
87 let above = pos.above();
88 if !world.get_block_state(above).is_air() || world.is_outside_build_height(above.y()) {
89 return;
90 }
91
92 let current_age = state.get_value(AGE);
93 if current_age >= DEAD_AGE {
94 return;
95 }
96
97 let mut grow_upwards = false;
98 let mut pillar_on_support_block = false;
99 let below = world.get_block_state(pos.below());
100 if below.get_block().has_tag(&BlockTag::SUPPORTS_CHORUS_FLOWER) {
101 grow_upwards = true;
102 } else if below.get_block() == self.plant {
103 let mut height = 1;
104 for _ in 0..PILLAR_SCAN_DEPTH {
105 let test_state = world.get_block_state(pos.below_n(height + 1));
106 if test_state.get_block() != self.plant {
107 pillar_on_support_block = test_state
108 .get_block()
109 .has_tag(&BlockTag::SUPPORTS_CHORUS_FLOWER);
110 break;
111 }
112 height += 1;
113 }
114
115 if height < MIN_PILLAR_HEIGHT
116 || height
117 <= rng.random_range(
118 0..if pillar_on_support_block {
119 GROWTH_RANDOM_RANGE_ON_SUPPORT
120 } else {
121 GROWTH_RANDOM_RANGE
122 },
123 )
124 {
125 grow_upwards = true;
126 }
127 } else if below.is_air() {
128 grow_upwards = true;
129 }
130
131 if grow_upwards
132 && Self::all_neighbors_empty(world, above, None)
133 && world.get_block_state(pos.above_n(2)).is_air()
134 {
135 world.set_block(
136 pos,
137 ChorusPlantBlock::state_with_connections(world, pos, self.plant.default_state()),
138 UpdateFlags::UPDATE_CLIENTS,
139 );
140 self.place_grown_flower(world, above, current_age);
141 } else if current_age < DEAD_AGE - 1 {
142 let mut branch_attempts = rng.random_range(0..BRANCH_ATTEMPT_RANDOM_RANGE);
143 if pillar_on_support_block {
144 branch_attempts += 1;
145 }
146
147 let mut created_branch = false;
148 for _ in 0..branch_attempts {
149 let direction =
150 HORIZONTAL_DIRECTIONS[rng.random_range(0..HORIZONTAL_DIRECTIONS.len())];
151 let target = pos.relative(direction);
152 if world.get_block_state(target).is_air()
153 && world.get_block_state(target.below()).is_air()
154 && Self::all_neighbors_empty(world, target, Some(direction.opposite()))
155 {
156 self.place_grown_flower(world, target, current_age + 1);
157 created_branch = true;
158 }
159 }
160
161 if created_branch {
162 world.set_block(
163 pos,
164 ChorusPlantBlock::state_with_connections(
165 world,
166 pos,
167 self.plant.default_state(),
168 ),
169 UpdateFlags::UPDATE_CLIENTS,
170 );
171 } else {
172 self.place_dead_flower(world, pos);
173 }
174 } else {
175 self.place_dead_flower(world, pos);
176 }
177 }
178}
179
180impl BlockBehavior for ChorusFlowerBlock {
181 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
182 let below_state = world.get_block_state(pos.below());
183 if below_state.get_block() == self.plant
184 || below_state
185 .get_block()
186 .has_tag(&BlockTag::SUPPORTS_CHORUS_FLOWER)
187 {
188 return true;
189 }
190
191 if !below_state.is_air() {
192 return false;
193 }
194
195 let mut has_single_plant_neighbor = false;
196 for direction in HORIZONTAL_DIRECTIONS {
197 let neighbor_state = world.get_block_state(pos.relative(direction));
198 if neighbor_state.get_block() == self.plant {
199 if has_single_plant_neighbor {
200 return false;
201 }
202 has_single_plant_neighbor = true;
203 } else if !neighbor_state.is_air() {
204 return false;
205 }
206 }
207
208 has_single_plant_neighbor
209 }
210
211 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
212 default_surviving_state(self.block, self, context)
213 }
214
215 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
216 if !self.can_survive(state, world, pos) {
217 world.destroy_block(pos, true);
218 }
219 }
220
221 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
222 self.random_tick_with_rng(state, world, pos, &mut rand::rng());
223 }
224
225 fn update_shape(
226 &self,
227 state: BlockStateId,
228 world: &dyn ScheduledTickAccess,
229 pos: BlockPos,
230 direction: Direction,
231 _neighbor_pos: BlockPos,
232 _neighbor_state: BlockStateId,
233 ) -> BlockStateId {
234 if direction != Direction::Up && !self.can_survive(state, world, pos) {
235 world.schedule_block_tick_default(pos, self.block, 1);
236 }
237 state
238 }
239
240 fn on_projectile_hit(
241 &self,
242 _state: BlockStateId,
243 world: &Arc<World>,
244 hit: &ClipHitResult,
245 projectile: &dyn Projectile,
246 ) {
247 if Self::projectile_can_break(projectile, world, hit.block_pos) {
248 world.destroy_block_by_entity(hit.block_pos, true, projectile.as_entity_event_source());
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use std::convert::Infallible;
256
257 use rand::TryRng;
258 use rand::{SeedableRng, rngs::StdRng};
259 use steel_registry::{init_vanilla_registry, vanilla_blocks};
260 use steel_utils::ChunkPos;
261
262 use crate::{
263 behavior::init_behaviors,
264 test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk},
265 };
266
267 use super::*;
268
269 fn behavior() -> ChorusFlowerBlock {
270 ChorusFlowerBlock::new(
271 &vanilla_blocks::CHORUS_FLOWER,
272 &vanilla_blocks::CHORUS_PLANT,
273 )
274 }
275
276 #[derive(Default)]
277 struct MaxRng;
278
279 impl TryRng for MaxRng {
280 type Error = Infallible;
281
282 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
283 Ok(u32::MAX)
284 }
285
286 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
287 Ok(u64::MAX)
288 }
289
290 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
291 dst.fill(u8::MAX);
292 Ok(())
293 }
294 }
295
296 #[test]
297 fn grows_upward_from_chorus_support() {
298 init_vanilla_registry();
299 init_behaviors();
300 let world_fixture = fresh_test_world("chorus_flower_growth");
301 let world = &world_fixture.world;
302 let pos = BlockPos::new(8, 64, 8);
303 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
304
305 assert!(world.set_block(
306 pos.below(),
307 vanilla_blocks::END_STONE.default_state(),
308 UpdateFlags::UPDATE_NONE,
309 ));
310 let state = vanilla_blocks::CHORUS_FLOWER.default_state();
311 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
312
313 behavior().random_tick_with_rng(state, world, pos, &mut StdRng::seed_from_u64(0));
314
315 let stem = world.get_block_state(pos);
316 assert_eq!(stem.get_block(), &vanilla_blocks::CHORUS_PLANT);
317 assert!(stem.get_value(&BlockStateProperties::UP));
318 let grown = world.get_block_state(pos.above());
319 assert_eq!(grown.get_block(), &vanilla_blocks::CHORUS_FLOWER);
320 assert_eq!(grown.get_value(AGE), 0);
321 }
322
323 #[test]
324 fn mature_flower_dies_when_it_cannot_grow() {
325 init_vanilla_registry();
326 init_behaviors();
327 let world_fixture = fresh_test_world("chorus_flower_death");
328 let world = &world_fixture.world;
329 let pos = BlockPos::new(8, 64, 8);
330 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
331
332 assert!(world.set_block(
333 pos.below(),
334 vanilla_blocks::STONE.default_state(),
335 UpdateFlags::UPDATE_NONE,
336 ));
337 let state = vanilla_blocks::CHORUS_FLOWER
338 .default_state()
339 .set_value(AGE, DEAD_AGE - 1);
340 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
341
342 behavior().random_tick_with_rng(state, world, pos, &mut StdRng::seed_from_u64(0));
343
344 assert_eq!(world.get_block_state(pos).get_value(AGE), DEAD_AGE);
345 }
346
347 #[test]
348 fn creates_branches_when_upward_growth_is_obstructed() {
349 init_vanilla_registry();
350 init_behaviors();
351 let world_fixture = fresh_test_world("chorus_flower_branching");
352 let world = &world_fixture.world;
353 let pos = BlockPos::new(8, 64, 8);
354 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
355
356 assert!(world.set_block(
357 pos.below(),
358 vanilla_blocks::END_STONE.default_state(),
359 UpdateFlags::UPDATE_NONE,
360 ));
361 assert!(world.set_block(
362 pos.above().north(),
363 vanilla_blocks::STONE.default_state(),
364 UpdateFlags::UPDATE_NONE,
365 ));
366 let state = vanilla_blocks::CHORUS_FLOWER.default_state();
367 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
368
369 behavior().random_tick_with_rng(state, world, pos, &mut MaxRng);
370
371 assert_eq!(
372 world.get_block_state(pos).get_block(),
373 &vanilla_blocks::CHORUS_PLANT
374 );
375 assert!(HORIZONTAL_DIRECTIONS.into_iter().any(|direction| {
376 let branch = world.get_block_state(pos.relative(direction));
377 branch.get_block() == &vanilla_blocks::CHORUS_FLOWER && branch.get_value(AGE) == 1
378 }));
379 }
380
381 #[test]
382 fn unsupported_flower_breaks_on_scheduled_tick() {
383 init_vanilla_registry();
384 init_behaviors();
385 let world_fixture = fresh_test_world("chorus_flower_survival");
386 let world = &world_fixture.world;
387 let pos = BlockPos::new(8, 64, 8);
388 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
389
390 assert!(world.set_block(
391 pos.below(),
392 vanilla_blocks::STONE.default_state(),
393 UpdateFlags::UPDATE_NONE,
394 ));
395 let state = vanilla_blocks::CHORUS_FLOWER.default_state();
396 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_NONE));
397
398 behavior().tick(state, world, pos);
399
400 assert!(world.get_block_state(pos).is_air());
401 }
402
403 #[test]
404 fn schedules_survival_tick_when_non_up_neighbor_changes() {
405 init_vanilla_registry();
406 let level = TestLevel::default();
407 let pos = BlockPos::ZERO;
408 let state = vanilla_blocks::CHORUS_FLOWER.default_state();
409
410 assert_eq!(
411 behavior().update_shape(
412 state,
413 &level,
414 pos,
415 Direction::Down,
416 pos.below(),
417 vanilla_blocks::AIR.default_state(),
418 ),
419 state
420 );
421
422 assert_eq!(level.scheduled_block_ticks.borrow().len(), 1);
423 let tick = level.scheduled_block_ticks.borrow()[0];
424 assert_eq!(tick.pos, pos);
425 assert_eq!(tick.block, &vanilla_blocks::CHORUS_FLOWER);
426 assert_eq!(tick.delay, 1);
427 }
428}