1use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::{
7 BlockRef, block_state_ext::BlockStateExt as _, properties::BlockStateProperties,
8 properties::Direction, shapes::VoxelShape,
9};
10use steel_registry::{vanilla_game_events, vanilla_items};
11use steel_utils::{
12 BlockPos, BlockStateId,
13 axis::Axis,
14 types::{InteractionHand, UpdateFlags},
15};
16
17use super::TripWireHookBlock;
18use crate::behavior::{BlockBehavior, BlockPlaceContext};
19use crate::entity::{Entity, InsideBlockEffectCollector};
20use crate::player::Player;
21use crate::world::{LevelReader, ScheduledTickAccess, World, game_event::GameEventContext};
22
23const WIRE_DISTANCE_MAX: i32 = 42;
24const RECHECK_PERIOD: i32 = 10;
25
26#[block_behavior]
28pub struct TripWireBlock {
29 block: BlockRef,
30 #[json_arg(vanilla_blocks)]
31 hook: BlockRef,
32}
33
34impl TripWireBlock {
35 #[must_use]
37 pub const fn new(block: BlockRef, hook: BlockRef) -> Self {
38 Self { block, hook }
39 }
40
41 fn should_connect_to(&self, state: BlockStateId, direction: Direction) -> bool {
42 if state.get_block() == self.hook {
43 state.get_value(&BlockStateProperties::HORIZONTAL_FACING) == direction.opposite()
44 } else {
45 state.get_block() == self.block
46 }
47 }
48
49 fn set_connection(state: BlockStateId, direction: Direction, connected: bool) -> BlockStateId {
50 match direction {
51 Direction::North => state.set_value(&BlockStateProperties::NORTH, connected),
52 Direction::East => state.set_value(&BlockStateProperties::EAST, connected),
53 Direction::South => state.set_value(&BlockStateProperties::SOUTH, connected),
54 Direction::West => state.set_value(&BlockStateProperties::WEST, connected),
55 Direction::Up | Direction::Down => state,
56 }
57 }
58
59 fn update_source(&self, world: &Arc<World>, pos: BlockPos, state: BlockStateId) {
60 for direction in [Direction::South, Direction::West] {
61 for distance in 1..WIRE_DISTANCE_MAX {
62 let test_pos = pos.relative_n(direction, distance);
63 let test_state = world.get_block_state(test_pos);
64 if test_state.get_block() == self.hook {
65 if test_state.get_value(&BlockStateProperties::HORIZONTAL_FACING)
66 == direction.opposite()
67 {
68 TripWireHookBlock::calculate_state(
69 world,
70 test_pos,
71 test_state,
72 false,
73 true,
74 distance,
75 Some(state),
76 );
77 }
78 break;
79 }
80 if test_state.get_block() != self.block {
81 break;
82 }
83 }
84 }
85 }
86
87 fn check_pressed_for_entity(&self, world: &Arc<World>, pos: BlockPos, entity: &dyn Entity) {
88 self.set_pressed(world, pos, !entity.is_ignoring_block_triggers());
89 }
90
91 fn check_pressed(&self, world: &Arc<World>, pos: BlockPos) {
92 let state = world.get_block_state(pos);
93 let Some(local_bounds) = state.get_outline_shape_at(pos).bounds() else {
94 self.set_pressed(world, pos, false);
95 return;
96 };
97 let bounds = local_bounds.at_block(pos);
98 let should_be_pressed = !world
99 .get_entities_in_aabb_matching(&bounds, |entity| !entity.is_ignoring_block_triggers())
100 .is_empty();
101 self.set_pressed(world, pos, should_be_pressed);
102 }
103
104 fn set_pressed(&self, world: &Arc<World>, pos: BlockPos, should_be_pressed: bool) {
105 let mut state = world.get_block_state(pos);
106 let was_pressed = state.get_value(&BlockStateProperties::POWERED);
107 if should_be_pressed != was_pressed {
108 state = state.set_value(&BlockStateProperties::POWERED, should_be_pressed);
109 world.set_block(pos, state, UpdateFlags::UPDATE_ALL);
110 self.update_source(world, pos, state);
111 }
112
113 if should_be_pressed {
114 world.schedule_block_tick_default(pos, self.block, RECHECK_PERIOD);
115 } else if was_pressed {
116 world.schedule_block_tick_default(pos, self.block, 0);
117 }
118 }
119}
120
121impl BlockBehavior for TripWireBlock {
122 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
123 let pos = context.place_pos();
124 let mut state = self.block.default_state();
125 for direction in [
126 Direction::North,
127 Direction::East,
128 Direction::South,
129 Direction::West,
130 ] {
131 state = Self::set_connection(
132 state,
133 direction,
134 self.should_connect_to(
135 context.world.get_block_state(pos.relative(direction)),
136 direction,
137 ),
138 );
139 }
140 Some(state)
141 }
142
143 fn update_shape(
144 &self,
145 state: BlockStateId,
146 _world: &dyn ScheduledTickAccess,
147 _pos: BlockPos,
148 direction: Direction,
149 _neighbor_pos: BlockPos,
150 neighbor_state: BlockStateId,
151 ) -> BlockStateId {
152 if direction.axis() == Axis::Y {
153 state
154 } else {
155 Self::set_connection(
156 state,
157 direction,
158 self.should_connect_to(neighbor_state, direction),
159 )
160 }
161 }
162
163 fn on_place(
164 &self,
165 state: BlockStateId,
166 world: &Arc<World>,
167 pos: BlockPos,
168 old_state: BlockStateId,
169 _moved_by_piston: bool,
170 ) {
171 if old_state.get_block() != self.block {
172 self.update_source(world, pos, state);
173 }
174 }
175
176 fn affect_neighbors_after_removal(
177 &self,
178 state: BlockStateId,
179 world: &Arc<World>,
180 pos: BlockPos,
181 moved_by_piston: bool,
182 ) {
183 if !moved_by_piston {
184 self.update_source(
185 world,
186 pos,
187 state.set_value(&BlockStateProperties::POWERED, true),
188 );
189 }
190 }
191
192 fn player_will_destroy(
193 &self,
194 state: BlockStateId,
195 world: &Arc<World>,
196 pos: BlockPos,
197 player: &Player,
198 ) -> BlockStateId {
199 let held_shears = {
200 let inventory = player.inventory.lock();
201 let main_hand = inventory.get_item_in_hand(InteractionHand::MainHand);
202 !main_hand.is_empty() && main_hand.is(&vanilla_items::SHEARS)
203 };
204 if held_shears {
205 world.set_block(
206 pos,
207 state.set_value(&BlockStateProperties::DISARMED, true),
208 UpdateFlags::UPDATE_NONE,
209 );
210 world.game_event(
211 &vanilla_game_events::SHEAR,
212 pos,
213 &GameEventContext::new(Some(player), None),
214 );
215 }
216 state
217 }
218
219 fn get_entity_inside_collision_shape(
220 &self,
221 state: BlockStateId,
222 _world: &dyn LevelReader,
223 _pos: BlockPos,
224 _entity: &dyn Entity,
225 ) -> VoxelShape {
226 state.get_static_outline_shape()
227 }
228
229 fn entity_inside(
230 &self,
231 state: BlockStateId,
232 world: &Arc<World>,
233 pos: BlockPos,
234 entity: &dyn Entity,
235 _effect_collector: &mut InsideBlockEffectCollector,
236 _is_precise: bool,
237 ) {
238 if !state.get_value(&BlockStateProperties::POWERED)
239 && !world.has_scheduled_block_tick(pos, self.block)
240 {
241 self.check_pressed_for_entity(world, pos, entity);
242 }
243 }
244
245 fn tick(&self, _state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
246 if world
247 .get_block_state(pos)
248 .get_value(&BlockStateProperties::POWERED)
249 {
250 self.check_pressed(world, pos);
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use std::sync::Arc;
258
259 use glam::DVec3;
260 use steel_registry::init_vanilla_registry;
261 use steel_registry::{vanilla_blocks, vanilla_entities};
262 use steel_utils::ChunkPos;
263
264 use super::*;
265 use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
266 use crate::entity::SharedEntity;
267 use crate::entity::entities::RawEntity;
268 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
269
270 #[test]
271 fn entity_inside_powers_wire_and_attached_hooks() {
272 init_vanilla_registry();
273 init_behaviors();
274 let world = fresh_test_world("tripwire_entity_inside");
275 let left = BlockPos::new(5, 64, 8);
276 let wire_pos = left.east();
277 let right = wire_pos.east();
278 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(left));
279 assert!(world.set_block(
280 left.west(),
281 vanilla_blocks::STONE.default_state(),
282 UpdateFlags::UPDATE_NONE,
283 ));
284 assert!(world.set_block(
285 right.east(),
286 vanilla_blocks::STONE.default_state(),
287 UpdateFlags::UPDATE_NONE,
288 ));
289 let left_state = vanilla_blocks::TRIPWIRE_HOOK
290 .default_state()
291 .set_value(&BlockStateProperties::HORIZONTAL_FACING, Direction::East);
292 let right_state = vanilla_blocks::TRIPWIRE_HOOK
293 .default_state()
294 .set_value(&BlockStateProperties::HORIZONTAL_FACING, Direction::West);
295 assert!(world.set_block(left, left_state, UpdateFlags::UPDATE_NONE));
296 assert!(world.set_block(right, right_state, UpdateFlags::UPDATE_NONE));
297 assert!(world.set_block(
298 wire_pos,
299 vanilla_blocks::TRIPWIRE.default_state(),
300 UpdateFlags::UPDATE_NONE,
301 ));
302 TripWireHookBlock::calculate_state(&world, left, left_state, false, false, -1, None);
303
304 let entity: SharedEntity = Arc::new(RawEntity::new(
305 7_002,
306 DVec3::new(6.5, 64.0, 8.5),
307 Arc::downgrade(&world),
308 &vanilla_entities::PIG,
309 ));
310 let mut effects = InsideBlockEffectCollector::new();
311 BLOCK_BEHAVIORS
312 .get_behavior(&vanilla_blocks::TRIPWIRE)
313 .entity_inside(
314 world.get_block_state(wire_pos),
315 &world,
316 wire_pos,
317 entity.as_ref(),
318 &mut effects,
319 true,
320 );
321
322 assert!(
323 world
324 .get_block_state(wire_pos)
325 .get_value(&BlockStateProperties::POWERED)
326 );
327 assert!(
328 world
329 .get_block_state(left)
330 .get_value(&BlockStateProperties::POWERED)
331 );
332 assert!(
333 world
334 .get_block_state(right)
335 .get_value(&BlockStateProperties::POWERED)
336 );
337 assert!(world.has_scheduled_block_tick(wire_pos, &vanilla_blocks::TRIPWIRE));
338 }
339}