steel_core/behavior/blocks/building/
fence_gate_block.rs1use crate::behavior::InventoryAccess;
9use crate::behavior::block::BlockBehavior;
10use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult};
11use crate::entity::Entity;
12use crate::entity::ai::path::PathComputationType;
13use crate::player::Player;
14use crate::world::game_event::GameEventContext;
15use crate::world::{ScheduledTickAccess, SignalGetter as _, World};
16use std::sync::Arc;
17use steel_macros::block_behavior;
18use steel_registry::blocks::BlockRef;
19use steel_registry::blocks::block_state_ext::BlockStateExt;
20use steel_registry::blocks::properties::{
21 BlockStateProperties, BoolProperty, Direction, EnumProperty,
22};
23use steel_registry::sound_event::SoundEventRef;
24use steel_registry::vanilla_block_tags::BlockTag;
25use steel_registry::vanilla_game_events;
26use steel_utils::axis::Axis;
27use steel_utils::types::UpdateFlags;
28use steel_utils::{BlockPos, BlockStateId};
29
30#[block_behavior]
32pub struct FenceGateBlock {
33 block: BlockRef,
34 #[json_arg(sound_events, json = "type_fence_gate_open")]
35 sound_open: SoundEventRef,
36 #[json_arg(sound_events, json = "type_fence_gate_close")]
37 sound_close: SoundEventRef,
38}
39
40const FACING: EnumProperty<Direction> = BlockStateProperties::HORIZONTAL_FACING;
42const OPEN: BoolProperty = BlockStateProperties::OPEN;
44const POWERED: BoolProperty = BlockStateProperties::POWERED;
46const IN_WALL: BoolProperty = BlockStateProperties::IN_WALL;
48
49impl FenceGateBlock {
50 #[must_use]
54 pub const fn new(
55 block: BlockRef,
56 sound_open: SoundEventRef,
57 sound_close: SoundEventRef,
58 ) -> Self {
59 Self {
60 block,
61 sound_open,
62 sound_close,
63 }
64 }
65
66 #[must_use]
71 pub fn connects_to_direction(state: BlockStateId, direction: Direction) -> bool {
72 state.get_value(&FACING).axis() == direction.rotate_y_clockwise().axis()
73 }
74
75 fn is_wall(state: BlockStateId) -> bool {
77 state.get_block().has_tag(&BlockTag::WALLS)
78 }
79}
80
81impl BlockBehavior for FenceGateBlock {
82 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
83 let world = context.world;
84 let pos = context.place_pos();
85 let direction = context.horizontal_direction();
86 let axis = direction.axis();
87
88 let in_wall = match axis {
89 Axis::Z => {
90 Self::is_wall(world.get_block_state(Direction::West.relative(pos)))
91 || Self::is_wall(world.get_block_state(Direction::East.relative(pos)))
92 }
93 Axis::X => {
94 Self::is_wall(world.get_block_state(Direction::North.relative(pos)))
95 || Self::is_wall(world.get_block_state(Direction::South.relative(pos)))
96 }
97 Axis::Y => false,
98 };
99
100 let is_open = world.has_neighbor_signal(pos);
101
102 Some(
103 self.block
104 .default_state()
105 .set_value(&FACING, direction)
106 .set_value(&OPEN, is_open)
107 .set_value(&POWERED, is_open)
108 .set_value(&IN_WALL, in_wall),
109 )
110 }
111
112 fn update_shape(
113 &self,
114 state: BlockStateId,
115 world: &dyn ScheduledTickAccess,
116 pos: BlockPos,
117 direction: Direction,
118 _neighbor_pos: BlockPos,
119 neighbor_state: BlockStateId,
120 ) -> BlockStateId {
121 if state.get_value(&FACING).rotate_y_clockwise().axis() != direction.axis() {
124 return state;
125 }
126 let opposite_neighbor = world.get_block_state(direction.opposite().relative(pos));
127 let in_wall = Self::is_wall(neighbor_state) || Self::is_wall(opposite_neighbor);
128 state.set_value(&IN_WALL, in_wall)
129 }
130
131 fn use_without_item(
132 &self,
133 state: BlockStateId,
134 world: &Arc<World>,
135 pos: BlockPos,
136 player: &Player,
137 _hit_result: &BlockHitResult,
138 _inv: &mut InventoryAccess,
139 ) -> InteractionResult {
140 let mut new_state = state;
141 if new_state.get_value(&OPEN) {
142 new_state = new_state.set_value(&OPEN, false);
143 } else {
144 let player_direction = player.direction_yaw();
145 if new_state.get_value(&FACING) == player_direction.opposite() {
147 new_state = new_state.set_value(&FACING, player_direction);
148 }
149 new_state = new_state.set_value(&OPEN, true);
150 }
151
152 world.set_block(
154 pos,
155 new_state,
156 UpdateFlags::UPDATE_CLIENTS | UpdateFlags::UPDATE_IMMEDIATE,
157 );
158
159 let opens = new_state.get_value(&OPEN);
160 let sound = if opens {
161 self.sound_open
162 } else {
163 self.sound_close
164 };
165 let pitch = rand::random::<f32>() * 0.1 + 0.9;
166 world.play_block_sound(sound, pos, 1.0, pitch, Some(player.id()));
167 let event = if opens {
168 &vanilla_game_events::BLOCK_OPEN
169 } else {
170 &vanilla_game_events::BLOCK_CLOSE
171 };
172 world.game_event(event, pos, &GameEventContext::new(Some(player), None));
173 InteractionResult::Success
174 }
175
176 fn handle_neighbor_changed(
177 &self,
178 state: BlockStateId,
179 world: &Arc<World>,
180 pos: BlockPos,
181 _source_block: BlockRef,
182 _moved_by_piston: bool,
183 ) {
184 let has_power = world.has_neighbor_signal(pos);
185 if state.get_value(&POWERED) == has_power {
186 return;
187 }
188
189 world.set_block(
190 pos,
191 state
192 .set_value(&POWERED, has_power)
193 .set_value(&OPEN, has_power),
194 UpdateFlags::UPDATE_CLIENTS,
195 );
196 if state.get_value(&OPEN) == has_power {
197 return;
198 }
199
200 let sound = if has_power {
201 self.sound_open
202 } else {
203 self.sound_close
204 };
205 let pitch = rand::random::<f32>() * 0.1 + 0.9;
206 world.play_block_sound(sound, pos, 1.0, pitch, None);
207 let event = if has_power {
208 &vanilla_game_events::BLOCK_OPEN
209 } else {
210 &vanilla_game_events::BLOCK_CLOSE
211 };
212 world.game_event(event, pos, &GameEventContext::default());
213 }
214
215 fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
216 match computation_type {
217 PathComputationType::Land | PathComputationType::Air => state.get_value(&OPEN),
218 PathComputationType::Water => false,
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use steel_registry::{
226 blocks::properties::BlockStateProperties, init_vanilla_registry, vanilla_blocks,
227 };
228 use steel_utils::{ChunkPos, types::UpdateFlags};
229
230 use super::*;
231 use crate::{
232 behavior::init_behaviors,
233 test_support::{fresh_test_world, insert_ready_full_chunk},
234 };
235
236 #[test]
237 fn redstone_power_opens_and_closes_fence_gate() {
238 init_vanilla_registry();
239 init_behaviors();
240 let world = fresh_test_world("fence_gate_redstone");
241 let pos = BlockPos::new(8, 64, 8);
242 let power_pos = pos.west();
243 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
244 assert!(world.set_block(
245 pos,
246 vanilla_blocks::OAK_FENCE_GATE.default_state(),
247 UpdateFlags::UPDATE_NONE,
248 ));
249
250 assert!(world.set_block(
251 power_pos,
252 vanilla_blocks::REDSTONE_BLOCK.default_state(),
253 UpdateFlags::UPDATE_ALL,
254 ));
255 let powered = world.get_block_state(pos);
256 assert!(powered.get_value(&BlockStateProperties::POWERED));
257 assert!(powered.get_value(&BlockStateProperties::OPEN));
258
259 assert!(world.remove_block(power_pos, false));
260 let unpowered = world.get_block_state(pos);
261 assert!(!unpowered.get_value(&BlockStateProperties::POWERED));
262 assert!(!unpowered.get_value(&BlockStateProperties::OPEN));
263 }
264}