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 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#[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 #[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}