Skip to main content

steel_core/behavior/blocks/redstone/
target_block.rs

1//! Vanilla projectile-sensitive target block behavior.
2
3use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::blocks::properties::BlockStateProperties;
9use steel_utils::axis::Axis;
10use steel_utils::types::UpdateFlags;
11use steel_utils::{BlockPos, BlockStateId};
12
13use crate::behavior::{BlockBehavior, BlockPlaceContext};
14use crate::entity::Entity;
15use crate::entity::projectile::Projectile;
16use crate::world::{ClipHitResult, LevelReader, SignalQueryContext, World};
17
18const ACTIVATION_TICKS_ARROWS: i32 = 20;
19const ACTIVATION_TICKS_OTHER: i32 = 8;
20const RESET_ON_PLACE_FLAGS: UpdateFlags =
21    UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
22
23/// Vanilla `TargetBlock` behavior.
24#[block_behavior]
25pub struct TargetBlock {
26    block: BlockRef,
27}
28
29impl TargetBlock {
30    /// Creates target block behavior.
31    #[must_use]
32    pub const fn new(block: BlockRef) -> Self {
33        Self { block }
34    }
35
36    fn distance_from_block_center(coordinate: f64) -> f64 {
37        (coordinate - coordinate.floor() - 0.5).abs()
38    }
39
40    fn redstone_strength(hit: &ClipHitResult) -> i32 {
41        let dist_x = Self::distance_from_block_center(hit.location.x);
42        let dist_y = Self::distance_from_block_center(hit.location.y);
43        let dist_z = Self::distance_from_block_center(hit.location.z);
44        let distance = match hit.direction.axis() {
45            Axis::Y => dist_x.max(dist_z),
46            Axis::Z => dist_x.max(dist_y),
47            Axis::X => dist_y.max(dist_z),
48        };
49        let centered = ((0.5 - distance) / 0.5).clamp(0.0, 1.0);
50        (15.0 * centered).ceil().max(1.0) as i32
51    }
52
53    fn update_redstone_output(
54        &self,
55        world: &Arc<World>,
56        state: BlockStateId,
57        hit: &ClipHitResult,
58        entity: &dyn Entity,
59    ) -> i32 {
60        let strength = Self::redstone_strength(hit);
61        if !world.has_scheduled_block_tick(hit.block_pos, self.block) {
62            world.set_block(
63                hit.block_pos,
64                state.set_value(&BlockStateProperties::POWER, strength as u8),
65                UpdateFlags::UPDATE_ALL,
66            );
67            world.schedule_block_tick_default(
68                hit.block_pos,
69                self.block,
70                if entity.is_abstract_arrow() {
71                    ACTIVATION_TICKS_ARROWS
72                } else {
73                    ACTIVATION_TICKS_OTHER
74                },
75            );
76        }
77        strength
78    }
79}
80
81impl BlockBehavior for TargetBlock {
82    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
83        Some(self.block.default_state())
84    }
85
86    fn on_projectile_hit(
87        &self,
88        state: BlockStateId,
89        world: &Arc<World>,
90        hit: &ClipHitResult,
91        projectile: &dyn Projectile,
92    ) {
93        let _strength = self.update_redstone_output(world, state, hit, projectile);
94        // The owner-facing target-hit stat and advancement criterion await
95        // Steel's shared statistics and advancement foundations.
96    }
97
98    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
99        if state.get_value(&BlockStateProperties::POWER) != 0 {
100            world.set_block(
101                pos,
102                state.set_value(&BlockStateProperties::POWER, 0_u8),
103                UpdateFlags::UPDATE_ALL,
104            );
105        }
106    }
107
108    fn on_place(
109        &self,
110        state: BlockStateId,
111        world: &Arc<World>,
112        pos: BlockPos,
113        old_state: BlockStateId,
114        _moved_by_piston: bool,
115    ) {
116        if old_state.get_block() != state.get_block()
117            && state.get_value(&BlockStateProperties::POWER) > 0
118            && !world.has_scheduled_block_tick(pos, self.block)
119        {
120            world.set_block(
121                pos,
122                state.set_value(&BlockStateProperties::POWER, 0_u8),
123                RESET_ON_PLACE_FLAGS,
124            );
125        }
126    }
127
128    fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
129        true
130    }
131
132    fn get_own_signal(
133        &self,
134        state: BlockStateId,
135        _world: &dyn LevelReader,
136        _pos: BlockPos,
137        _context: SignalQueryContext,
138    ) -> i32 {
139        i32::from(state.get_value(&BlockStateProperties::POWER))
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use glam::DVec3;
146    use steel_registry::blocks::properties::Direction;
147    use steel_registry::init_vanilla_registry;
148
149    use super::*;
150
151    fn hit(location: DVec3, direction: Direction) -> ClipHitResult {
152        ClipHitResult {
153            location,
154            direction,
155            block_pos: BlockPos::new(-1, 64, -1),
156            miss: false,
157            inside: false,
158            world_border_hit: false,
159        }
160    }
161
162    #[test]
163    fn hit_strength_uses_the_hit_face_and_floor_based_fraction() {
164        init_vanilla_registry();
165
166        assert_eq!(
167            TargetBlock::redstone_strength(&hit(DVec3::new(-0.5, 64.99, -0.5), Direction::Up,)),
168            15
169        );
170        assert_eq!(
171            TargetBlock::redstone_strength(&hit(DVec3::new(-0.01, 64.5, -0.5), Direction::Up,)),
172            1
173        );
174        assert_eq!(
175            TargetBlock::redstone_strength(&hit(DVec3::new(-0.5, 64.5, -0.25), Direction::North,)),
176            15
177        );
178    }
179}