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, IntProperty};
9use steel_registry::vanilla_custom_stats;
10use steel_utils::axis::Axis;
11use steel_utils::types::UpdateFlags;
12use steel_utils::{BlockPos, BlockStateId};
13
14use crate::behavior::blocks::redstone::{MAX_REDSTONE_SIGNAL, MIN_REDSTONE_SIGNAL};
15use crate::behavior::{BlockBehavior, BlockPlaceContext};
16use crate::entity::Entity;
17use crate::entity::projectile::Projectile;
18use crate::world::{ClipHitResult, LevelReader, SignalQueryContext, World};
19
20const ACTIVATION_TICKS_ARROWS: i32 = 20;
21const ACTIVATION_TICKS_OTHER: i32 = 8;
22const RESET_ON_PLACE_FLAGS: UpdateFlags =
23    UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
24
25/// Vanilla `TargetBlock` behavior.
26#[block_behavior]
27pub struct TargetBlock {
28    block: BlockRef,
29}
30
31const POWER: &IntProperty = &BlockStateProperties::POWER;
32
33impl TargetBlock {
34    /// Creates target block behavior.
35    #[must_use]
36    pub const fn new(block: BlockRef) -> Self {
37        Self { block }
38    }
39
40    fn distance_from_block_center(coordinate: f64) -> f64 {
41        (coordinate - coordinate.floor() - 0.5).abs()
42    }
43
44    fn redstone_strength(hit: &ClipHitResult) -> i32 {
45        let dist_x = Self::distance_from_block_center(hit.location.x);
46        let dist_y = Self::distance_from_block_center(hit.location.y);
47        let dist_z = Self::distance_from_block_center(hit.location.z);
48        let distance = match hit.direction.axis() {
49            Axis::Y => dist_x.max(dist_z),
50            Axis::Z => dist_x.max(dist_y),
51            Axis::X => dist_y.max(dist_z),
52        };
53        let centered = ((0.5 - distance) / 0.5).clamp(0.0, 1.0);
54        (f64::from(MAX_REDSTONE_SIGNAL) * centered).ceil().max(1.0) as i32
55    }
56
57    fn update_redstone_output(
58        &self,
59        world: &Arc<World>,
60        state: BlockStateId,
61        hit: &ClipHitResult,
62        entity: &dyn Entity,
63    ) -> i32 {
64        let strength = Self::redstone_strength(hit);
65        if !world.has_scheduled_block_tick(hit.block_pos, self.block) {
66            world.set_block(
67                hit.block_pos,
68                state.set_value(POWER, strength as u8),
69                UpdateFlags::UPDATE_ALL,
70            );
71            world.schedule_block_tick_default(
72                hit.block_pos,
73                self.block,
74                if entity.is_abstract_arrow() {
75                    ACTIVATION_TICKS_ARROWS
76                } else {
77                    ACTIVATION_TICKS_OTHER
78                },
79            );
80        }
81        strength
82    }
83}
84
85impl BlockBehavior for TargetBlock {
86    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
87        Some(self.block.default_state())
88    }
89
90    fn on_projectile_hit(
91        &self,
92        state: BlockStateId,
93        world: &Arc<World>,
94        hit: &ClipHitResult,
95        projectile: &dyn Projectile,
96    ) {
97        let _strength = self.update_redstone_output(world, state, hit, projectile);
98        if let Some(owner) = projectile.projectile_owner()
99            && let Some(player) = owner.as_player()
100        {
101            player.award_custom_stat(&vanilla_custom_stats::TARGET_HIT);
102            // TODO: The advancement criterion awaits Steel's shared advancement foundations.
103        }
104    }
105
106    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
107        if state.get_value(POWER) != MIN_REDSTONE_SIGNAL as u8 {
108            world.set_block(pos, state.set_value(POWER, 0_u8), UpdateFlags::UPDATE_ALL);
109        }
110    }
111
112    fn on_place(
113        &self,
114        state: BlockStateId,
115        world: &Arc<World>,
116        pos: BlockPos,
117        old_state: BlockStateId,
118        _moved_by_piston: bool,
119    ) {
120        if old_state.get_block() != state.get_block()
121            && i32::from(state.get_value(POWER)) > MIN_REDSTONE_SIGNAL
122            && !world.has_scheduled_block_tick(pos, self.block)
123        {
124            world.set_block(pos, state.set_value(POWER, 0_u8), RESET_ON_PLACE_FLAGS);
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(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}