steel_core/behavior/blocks/redstone/
target_block.rs1use 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#[block_behavior]
27pub struct TargetBlock {
28 block: BlockRef,
29}
30
31const POWER: &IntProperty = &BlockStateProperties::POWER;
32
33impl TargetBlock {
34 #[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 }
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}