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]
69 pub fn connects_to_direction(state: BlockStateId, direction: Direction) -> bool {
70 state.get_value(FACING).axis() == direction.rotate_y_clockwise().axis()
71 }
72
73 fn is_wall(state: BlockStateId) -> bool {
74 state.get_block().has_tag(&BlockTag::WALLS)
75 }
76}
77
78impl BlockBehavior for FenceGateBlock {
79 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
80 let world = context.world;
81 let pos = context.place_pos();
82 let direction = context.horizontal_direction();
83 let axis = direction.axis();
84
85 let in_wall = match axis {
86 Axis::Z => {
87 Self::is_wall(world.get_block_state(Direction::West.relative(pos)))
88 || Self::is_wall(world.get_block_state(Direction::East.relative(pos)))
89 }
90 Axis::X => {
91 Self::is_wall(world.get_block_state(Direction::North.relative(pos)))
92 || Self::is_wall(world.get_block_state(Direction::South.relative(pos)))
93 }
94 Axis::Y => false,
95 };
96
97 let is_open = world.has_neighbor_signal(pos);
98
99 Some(
100 self.block
101 .default_state()
102 .set_value(FACING, direction)
103 .set_value(OPEN, is_open)
104 .set_value(POWERED, is_open)
105 .set_value(IN_WALL, in_wall),
106 )
107 }
108
109 fn update_shape(
110 &self,
111 state: BlockStateId,
112 world: &dyn ScheduledTickAccess,
113 pos: BlockPos,
114 direction: Direction,
115 _neighbor_pos: BlockPos,
116 neighbor_state: BlockStateId,
117 ) -> BlockStateId {
118 if state.get_value(FACING).rotate_y_clockwise().axis() != direction.axis() {
121 return state;
122 }
123 let opposite_neighbor = world.get_block_state(direction.opposite().relative(pos));
124 let in_wall = Self::is_wall(neighbor_state) || Self::is_wall(opposite_neighbor);
125 state.set_value(IN_WALL, in_wall)
126 }
127
128 fn use_without_item(
129 &self,
130 state: BlockStateId,
131 world: &Arc<World>,
132 pos: BlockPos,
133 player: &Player,
134 _hit_result: &BlockHitResult,
135 _inv: &mut InventoryAccess,
136 ) -> InteractionResult {
137 let mut new_state = state;
138 if new_state.get_value(OPEN) {
139 new_state = new_state.set_value(OPEN, false);
140 } else {
141 let player_direction = player.direction_yaw();
142 if new_state.get_value(FACING) == player_direction.opposite() {
144 new_state = new_state.set_value(FACING, player_direction);
145 }
146 new_state = new_state.set_value(OPEN, true);
147 }
148
149 world.set_block(
151 pos,
152 new_state,
153 UpdateFlags::UPDATE_CLIENTS | UpdateFlags::UPDATE_IMMEDIATE,
154 );
155
156 let opens = new_state.get_value(OPEN);
157 let sound = if opens {
158 self.sound_open
159 } else {
160 self.sound_close
161 };
162 let pitch = rand::random_range(0.9..1.0);
163 world.play_block_sound(sound, pos, 1.0, pitch, Some(player.id()));
164 let event = if opens {
165 &vanilla_game_events::BLOCK_OPEN
166 } else {
167 &vanilla_game_events::BLOCK_CLOSE
168 };
169 world.game_event(event, pos, &GameEventContext::new(Some(player), None));
170 InteractionResult::Success
171 }
172
173 fn handle_neighbor_changed(
174 &self,
175 state: BlockStateId,
176 world: &Arc<World>,
177 pos: BlockPos,
178 _source_block: BlockRef,
179 _moved_by_piston: bool,
180 ) {
181 let has_power = world.has_neighbor_signal(pos);
182 if state.get_value(POWERED) == has_power {
183 return;
184 }
185
186 world.set_block(
187 pos,
188 state
189 .set_value(POWERED, has_power)
190 .set_value(OPEN, has_power),
191 UpdateFlags::UPDATE_CLIENTS,
192 );
193 if state.get_value(OPEN) == has_power {
194 return;
195 }
196
197 let sound = if has_power {
198 self.sound_open
199 } else {
200 self.sound_close
201 };
202 let pitch = rand::random_range(0.9..1.0);
203 world.play_block_sound(sound, pos, 1.0, pitch, None);
204 let event = if has_power {
205 &vanilla_game_events::BLOCK_OPEN
206 } else {
207 &vanilla_game_events::BLOCK_CLOSE
208 };
209 world.game_event(event, pos, &GameEventContext::default());
210 }
211
212 fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
213 match computation_type {
214 PathComputationType::Land | PathComputationType::Air => state.get_value(OPEN),
215 PathComputationType::Water => false,
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use steel_registry::{init_vanilla_registry, vanilla_blocks};
223 use steel_utils::{ChunkPos, types::UpdateFlags};
224
225 use super::*;
226 use crate::{
227 behavior::init_behaviors,
228 test_support::{fresh_test_world, insert_ready_full_chunk},
229 };
230
231 #[test]
232 fn redstone_power_opens_and_closes_fence_gate() {
233 init_vanilla_registry();
234 init_behaviors();
235 let world_fixture = fresh_test_world("fence_gate_redstone");
236 let world = &world_fixture.world;
237 let pos = BlockPos::new(8, 64, 8);
238 let power_pos = pos.west();
239 insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
240 assert!(world.set_block(
241 pos,
242 vanilla_blocks::OAK_FENCE_GATE.default_state(),
243 UpdateFlags::UPDATE_NONE,
244 ));
245
246 assert!(world.set_block(
247 power_pos,
248 vanilla_blocks::REDSTONE_BLOCK.default_state(),
249 UpdateFlags::UPDATE_ALL,
250 ));
251 let powered = world.get_block_state(pos);
252 assert!(powered.get_value(POWERED));
253 assert!(powered.get_value(OPEN));
254
255 assert!(world.remove_block(power_pos, false));
256 let unpowered = world.get_block_state(pos);
257 assert!(!unpowered.get_value(POWERED));
258 assert!(!unpowered.get_value(OPEN));
259 }
260}