Skip to main content

steel_core/behavior/blocks/vegetation/
attached_stem_block.rs

1use steel_macros::block_behavior;
2use steel_registry::{
3    blocks::{
4        BlockRef,
5        block_state_ext::BlockStateExt,
6        properties::{BlockStateProperties, EnumProperty, IntProperty},
7    },
8    item_stack::ItemStack,
9    items::ItemRef,
10};
11use steel_utils::{BlockPos, BlockStateId, Direction, Identifier};
12
13use crate::{
14    behavior::{
15        BlockBehavior, BlockPlaceContext,
16        blocks::vegetation::{
17            Vegetation, default_surviving_state,
18            vegetation_block::{survival_update_shape, vegetation_can_survive},
19        },
20    },
21    world::{LevelReader, ScheduledTickAccess},
22};
23
24const AGE: &IntProperty = &BlockStateProperties::AGE_7;
25const FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
26const MAX_AGE: u8 = 7;
27
28/// Vanilla attached pumpkin and melon stem behavior.
29#[block_behavior]
30pub struct AttachedStemBlock {
31    block: BlockRef,
32    #[json_arg(vanilla_blocks)]
33    stem: BlockRef,
34    #[json_arg(vanilla_blocks)]
35    fruit: BlockRef,
36    #[json_arg(vanilla_items)]
37    seed: ItemRef,
38    #[json_arg(vanilla_block_tags)]
39    support_blocks: Identifier,
40}
41
42impl AttachedStemBlock {
43    /// Creates an attached stem with its extracted stem, fruit, seed, and support tag.
44    #[must_use]
45    pub const fn new(
46        block: BlockRef,
47        stem: BlockRef,
48        fruit: BlockRef,
49        seed: ItemRef,
50        support_blocks: Identifier,
51    ) -> Self {
52        Self {
53            block,
54            stem,
55            fruit,
56            seed,
57            support_blocks,
58        }
59    }
60}
61
62impl BlockBehavior for AttachedStemBlock {
63    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
64        default_surviving_state(self.block, self, context)
65    }
66
67    fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
68        vegetation_can_survive(self, state, world, pos)
69    }
70
71    fn update_shape(
72        &self,
73        state: BlockStateId,
74        world: &dyn ScheduledTickAccess,
75        pos: BlockPos,
76        direction: Direction,
77        _neighbor_pos: BlockPos,
78        neighbor_state: BlockStateId,
79    ) -> BlockStateId {
80        if direction == state.get_value(FACING) && neighbor_state.get_block() != self.fruit {
81            return self.stem.default_state().set_value(AGE, MAX_AGE);
82        }
83
84        survival_update_shape(self, state, world, pos)
85    }
86
87    fn get_clone_item_stack(
88        &self,
89        _block: BlockRef,
90        _state: BlockStateId,
91        _include_data: bool,
92    ) -> Option<ItemStack> {
93        Some(ItemStack::new(self.seed))
94    }
95}
96
97impl Vegetation for AttachedStemBlock {
98    fn may_place_on(&self, state: BlockStateId, _world: &dyn LevelReader, _pos: BlockPos) -> bool {
99        state.get_block().has_tag(&self.support_blocks)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use steel_registry::{
106        init_vanilla_registry, vanilla_block_tags::BlockTag, vanilla_blocks, vanilla_items,
107    };
108
109    use crate::{
110        behavior::{BLOCK_BEHAVIORS, init_behaviors},
111        test_support::TestLevel,
112    };
113
114    use super::*;
115
116    fn pumpkin_attached() -> AttachedStemBlock {
117        AttachedStemBlock::new(
118            &vanilla_blocks::ATTACHED_PUMPKIN_STEM,
119            &vanilla_blocks::PUMPKIN_STEM,
120            &vanilla_blocks::PUMPKIN,
121            &vanilla_items::PUMPKIN_SEEDS,
122            BlockTag::SUPPORTS_PUMPKIN_STEM,
123        )
124    }
125
126    #[test]
127    fn attached_stem_keeps_its_fruit_and_reverts_to_mature_stem() {
128        init_vanilla_registry();
129        let behavior = pumpkin_attached();
130        let pos = BlockPos::ZERO;
131        let fruit_pos = pos.north();
132        let level = TestLevel::default()
133            .with_block(pos.below(), vanilla_blocks::FARMLAND.default_state())
134            .with_block(fruit_pos, vanilla_blocks::PUMPKIN.default_state());
135        let attached = vanilla_blocks::ATTACHED_PUMPKIN_STEM.default_state();
136
137        assert_eq!(
138            behavior.update_shape(
139                attached,
140                &level,
141                pos,
142                Direction::North,
143                fruit_pos,
144                vanilla_blocks::PUMPKIN.default_state(),
145            ),
146            attached
147        );
148
149        let reverted = behavior.update_shape(
150            attached,
151            &level,
152            pos,
153            Direction::North,
154            fruit_pos,
155            vanilla_blocks::AIR.default_state(),
156        );
157        assert_eq!(reverted.get_block(), &vanilla_blocks::PUMPKIN_STEM);
158        assert_eq!(reverted.get_value(AGE), MAX_AGE);
159    }
160
161    #[test]
162    fn attached_stem_breaks_when_its_support_disappears() {
163        init_vanilla_registry();
164        let behavior = pumpkin_attached();
165        let attached = vanilla_blocks::ATTACHED_PUMPKIN_STEM.default_state();
166        let unsupported = TestLevel::default();
167
168        let updated = behavior.update_shape(
169            attached,
170            &unsupported,
171            BlockPos::ZERO,
172            Direction::Down,
173            BlockPos::ZERO.below(),
174            vanilla_blocks::AIR.default_state(),
175        );
176        assert!(updated.is_air());
177    }
178
179    #[test]
180    fn registered_attached_mappings_are_not_swappable() {
181        init_vanilla_registry();
182        init_behaviors();
183        let cases = [
184            (
185                &vanilla_blocks::ATTACHED_PUMPKIN_STEM,
186                &vanilla_blocks::PUMPKIN,
187                &vanilla_blocks::MELON,
188                &vanilla_blocks::PUMPKIN_STEM,
189                &*vanilla_items::PUMPKIN_SEEDS,
190            ),
191            (
192                &vanilla_blocks::ATTACHED_MELON_STEM,
193                &vanilla_blocks::MELON,
194                &vanilla_blocks::PUMPKIN,
195                &vanilla_blocks::MELON_STEM,
196                &*vanilla_items::MELON_SEEDS,
197            ),
198        ];
199
200        for (attached_block, fruit, wrong_fruit, stem, seed) in cases {
201            let behavior = BLOCK_BEHAVIORS.get_behavior(attached_block);
202            let pos = BlockPos::ZERO;
203            let fruit_pos = pos.north();
204            let level = TestLevel::default()
205                .with_block(pos.below(), vanilla_blocks::FARMLAND.default_state())
206                .with_block(fruit_pos, fruit.default_state());
207            let attached = attached_block.default_state();
208
209            assert_eq!(
210                behavior.update_shape(
211                    attached,
212                    &level,
213                    pos,
214                    Direction::North,
215                    fruit_pos,
216                    fruit.default_state(),
217                ),
218                attached
219            );
220            let reverted = behavior.update_shape(
221                attached,
222                &level,
223                pos,
224                Direction::North,
225                fruit_pos,
226                wrong_fruit.default_state(),
227            );
228            assert_eq!(reverted.get_block(), stem);
229            assert_eq!(reverted.get_value(AGE), MAX_AGE);
230
231            let clone = behavior
232                .get_clone_item_stack(attached_block, attached, false)
233                .expect("attached stem has a clone item");
234            assert_eq!(clone.item(), seed);
235        }
236    }
237}