steel_core/behavior/blocks/vegetation/
attached_stem_block.rs1use 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#[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 #[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}