1pub use crate::blocks::properties::NoteBlockInstrument;
7use glam::DVec3;
8use steel_utils::{BlockPos, random::get_seed};
9
10use crate::sound_types::SoundType;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PushReaction {
15 Normal,
16 Destroy,
17 Block,
18 Ignore,
19 PushOnly,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum OffsetType {
25 None,
26 Xz,
27 Xyz,
28}
29
30#[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 #[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 #[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}