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