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