Skip to main content

steel_registry/blocks/
behavior.rs

1//! Block configuration and static properties.
2//!
3//! This module contains constant/static data about blocks. Dynamic behavior
4//! has been moved to `steel-core::behavior`.
5
6pub use crate::blocks::properties::NoteBlockInstrument;
7use glam::DVec3;
8use steel_utils::{BlockPos, random::get_seed};
9
10use crate::sound_types::SoundType;
11
12/// How a block reacts when pushed by a piston.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PushReaction {
15    Normal,
16    Destroy,
17    Block,
18    Ignore,
19    PushOnly,
20}
21
22/// Vanilla `BlockBehavior.OffsetType`.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum OffsetType {
25    None,
26    Xz,
27    Xyz,
28}
29
30/// Static configuration for a block type.
31///
32/// This contains constant properties that don't change based on game state.
33/// Dynamic behavior is handled by `BlockBehavior` in steel-core.
34#[derive(Debug)]
35pub struct BlockConfig {
36    pub has_collision: bool,
37    pub can_occlude: bool,
38    pub explosion_resistance: f32,
39    pub is_randomly_ticking: bool,
40    pub force_solid_off: bool,
41    pub force_solid_on: bool,
42    pub push_reaction: PushReaction,
43    pub friction: f32,
44    pub speed_factor: f32,
45    pub jump_factor: f32,
46    pub dynamic_shape: bool,
47    pub offset_type: OffsetType,
48    pub max_horizontal_offset: f32,
49    pub max_vertical_offset: f32,
50    pub destroy_time: f32,
51    pub ignited_by_lava: bool,
52    pub liquid: bool,
53    pub is_air: bool,
54    pub requires_correct_tool_for_drops: bool,
55    pub instrument: NoteBlockInstrument,
56    pub replaceable: bool,
57    pub sound_type: SoundType,
58}
59
60impl BlockConfig {
61    /// Starts building a new set of block properties.
62    #[must_use]
63    pub const fn new() -> Self {
64        Self {
65            has_collision: true,
66            can_occlude: true,
67            explosion_resistance: 0.0,
68            is_randomly_ticking: false,
69            force_solid_off: false,
70            force_solid_on: false,
71            push_reaction: PushReaction::Normal,
72            friction: 0.6,
73            speed_factor: 1.0,
74            jump_factor: 1.0,
75            dynamic_shape: false,
76            offset_type: OffsetType::None,
77            max_horizontal_offset: 0.25,
78            max_vertical_offset: 0.2,
79            destroy_time: 0.0,
80            ignited_by_lava: false,
81            liquid: false,
82            is_air: false,
83            requires_correct_tool_for_drops: false,
84            instrument: NoteBlockInstrument::Harp,
85            replaceable: false,
86            sound_type: crate::sound_types::STONE,
87        }
88    }
89
90    #[must_use]
91    pub const fn has_collision(mut self, has_collision: bool) -> Self {
92        self.has_collision = has_collision;
93        self
94    }
95
96    #[must_use]
97    pub const fn can_occlude(mut self, can_occlude: bool) -> Self {
98        self.can_occlude = can_occlude;
99        self
100    }
101
102    #[must_use]
103    pub const fn explosion_resistance(mut self, resistance: f32) -> Self {
104        self.explosion_resistance = resistance;
105        self
106    }
107
108    #[must_use]
109    pub const fn set_is_randomly_ticking(mut self, ticking: bool) -> Self {
110        self.is_randomly_ticking = ticking;
111        self
112    }
113
114    #[must_use]
115    pub const fn force_solid_off(mut self, force: bool) -> Self {
116        self.force_solid_off = force;
117        self
118    }
119
120    #[must_use]
121    pub const fn force_solid_on(mut self, force: bool) -> Self {
122        self.force_solid_on = force;
123        self
124    }
125
126    #[must_use]
127    pub const fn push_reaction(mut self, reaction: PushReaction) -> Self {
128        self.push_reaction = reaction;
129        self
130    }
131
132    #[must_use]
133    pub const fn friction(mut self, friction: f32) -> Self {
134        self.friction = friction;
135        self
136    }
137
138    #[must_use]
139    pub const fn speed_factor(mut self, factor: f32) -> Self {
140        self.speed_factor = factor;
141        self
142    }
143
144    #[must_use]
145    pub const fn jump_factor(mut self, factor: f32) -> Self {
146        self.jump_factor = factor;
147        self
148    }
149
150    #[must_use]
151    pub const fn dynamic_shape(mut self, dynamic: bool) -> Self {
152        self.dynamic_shape = dynamic;
153        self
154    }
155
156    #[must_use]
157    pub const fn offset_type(mut self, offset_type: OffsetType) -> Self {
158        self.offset_type = offset_type;
159        self
160    }
161
162    #[must_use]
163    pub const fn max_horizontal_offset(mut self, offset: f32) -> Self {
164        self.max_horizontal_offset = offset;
165        self
166    }
167
168    #[must_use]
169    pub const fn max_vertical_offset(mut self, offset: f32) -> Self {
170        self.max_vertical_offset = offset;
171        self
172    }
173
174    #[must_use]
175    pub const fn destroy_time(mut self, time: f32) -> Self {
176        self.destroy_time = time;
177        self
178    }
179
180    #[must_use]
181    pub const fn ignited_by_lava(mut self, ignited: bool) -> Self {
182        self.ignited_by_lava = ignited;
183        self
184    }
185
186    #[must_use]
187    pub const fn liquid(mut self, liquid: bool) -> Self {
188        self.liquid = liquid;
189        self
190    }
191
192    #[must_use]
193    pub const fn set_is_air(mut self, is_air: bool) -> Self {
194        self.is_air = is_air;
195        self
196    }
197
198    #[must_use]
199    pub const fn requires_correct_tool_for_drops(mut self, requires: bool) -> Self {
200        self.requires_correct_tool_for_drops = requires;
201        self
202    }
203
204    #[must_use]
205    pub const fn instrument(mut self, instrument: NoteBlockInstrument) -> Self {
206        self.instrument = instrument;
207        self
208    }
209
210    #[must_use]
211    pub const fn replaceable(mut self, replaceable: bool) -> Self {
212        self.replaceable = replaceable;
213        self
214    }
215
216    #[must_use]
217    pub const fn sound_type(mut self, sound_type: SoundType) -> Self {
218        self.sound_type = sound_type;
219        self
220    }
221
222    /// Returns the vanilla positional offset for this block config.
223    #[must_use]
224    pub fn offset_at(&self, pos: BlockPos) -> DVec3 {
225        let seed = get_seed(pos.x(), 0, pos.z());
226        let x = horizontal_offset_component(seed & 15, self.max_horizontal_offset);
227        let z = horizontal_offset_component((seed >> 8) & 15, self.max_horizontal_offset);
228
229        match self.offset_type {
230            OffsetType::None => DVec3::ZERO,
231            OffsetType::Xz => DVec3::new(x, 0.0, z),
232            OffsetType::Xyz => {
233                let y = (f64::from(((seed >> 4) & 15) as f32 / 15.0) - 1.0)
234                    * f64::from(self.max_vertical_offset);
235                DVec3::new(x, y, z)
236            }
237        }
238    }
239}
240
241impl Default for BlockConfig {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247fn horizontal_offset_component(seed_bits: i64, max_horizontal_offset: f32) -> f64 {
248    let raw_offset = (f64::from(seed_bits as f32 / 15.0) - 0.5) * 0.5;
249    raw_offset.clamp(
250        -f64::from(max_horizontal_offset),
251        f64::from(max_horizontal_offset),
252    )
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn assert_offset(config: &BlockConfig, pos: BlockPos, expected: DVec3) {
260        let offset = config.offset_at(pos);
261        assert!((offset - expected).length() < 1.0e-7);
262    }
263
264    #[test]
265    fn xz_offset_matches_vanilla_clamping() {
266        let config = BlockConfig::new()
267            .offset_type(OffsetType::Xz)
268            .max_horizontal_offset(0.125);
269
270        assert_offset(&config, BlockPos::ZERO, DVec3::new(-0.125, 0.0, -0.125));
271        assert_offset(
272            &config,
273            BlockPos::new(12, 64, 34),
274            DVec3::new(0.125, 0.0, 0.125),
275        );
276    }
277
278    #[test]
279    fn xz_offset_uses_block_position_seed_without_y() {
280        let config = BlockConfig::new()
281            .offset_type(OffsetType::Xz)
282            .max_horizontal_offset(0.125);
283
284        assert_offset(
285            &config,
286            BlockPos::new(1, 64, 0),
287            DVec3::new(0.116_666_666_666_666_64, 0.0, -0.016_666_666_666_666_663),
288        );
289        assert_offset(
290            &config,
291            BlockPos::new(-5, 12, 7),
292            DVec3::new(-0.049_999_999_999_999_99, 0.0, 0.116_666_666_666_666_64),
293        );
294    }
295
296    #[test]
297    fn xyz_offset_applies_vanilla_vertical_component() {
298        let config = BlockConfig::new()
299            .offset_type(OffsetType::Xyz)
300            .max_horizontal_offset(0.25)
301            .max_vertical_offset(0.1);
302
303        assert_offset(&config, BlockPos::ZERO, DVec3::new(-0.25, -0.1, -0.25));
304        assert_offset(
305            &config,
306            BlockPos::new(3, 40, 9),
307            DVec3::new(
308                -0.049_999_999_999_999_99,
309                -0.066_666_666_666_666_68,
310                -0.049_999_999_999_999_99,
311            ),
312        );
313    }
314}