1use std::{error::Error, fmt};
4
5use glam::DVec3;
6use steel_utils::{BlockPos, WorldAabb};
7
8use crate::level_data::WorldBorderData;
9
10const MAX_CENTER_COORDINATE: f64 = 2.999_998_4E7;
11const DEFAULT_ABSOLUTE_MAX_SIZE: i32 = 29_999_984;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub(crate) enum BorderStatus {
15 Growing,
16 Shrinking,
17 Stationary,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub(crate) struct WorldBorderSnapshot {
22 pub(crate) center_x: f64,
23 pub(crate) center_z: f64,
24 pub(crate) old_size: f64,
25 pub(crate) new_size: f64,
26 pub(crate) lerp_time: i64,
27 pub(crate) absolute_max_size: i32,
28 pub(crate) warning_blocks: i32,
29 pub(crate) warning_time: i32,
30 pub(crate) damage_per_block: f64,
31 pub(crate) safe_zone: f64,
32 pub(crate) min_x: f64,
33 pub(crate) min_z: f64,
34 pub(crate) max_x: f64,
35 pub(crate) max_z: f64,
36 pub(crate) status: BorderStatus,
37}
38
39impl WorldBorderSnapshot {
40 #[must_use]
41 pub(crate) fn is_block_within_bounds(self, pos: BlockPos) -> bool {
42 self.is_within_bounds_with_margin(f64::from(pos.x()), f64::from(pos.z()), 0.0)
43 }
44
45 #[must_use]
46 pub(crate) fn is_within_bounds_with_margin(self, x: f64, z: f64, margin: f64) -> bool {
47 x >= self.min_x - margin
48 && x < self.max_x + margin
49 && z >= self.min_z - margin
50 && z < self.max_z + margin
51 }
52
53 #[must_use]
54 pub(crate) fn clamp_to_bounds(self, x: f64, y: f64, z: f64) -> BlockPos {
55 BlockPos::from(self.clamp_vec3_to_bound(DVec3::new(x, y, z)))
56 }
57
58 #[must_use]
59 pub(crate) fn clamp_vec3_to_bound(self, position: DVec3) -> DVec3 {
60 let epsilon = f64::from(1.0E-5_f32);
61 DVec3::new(
62 clamp_f64(position.x, self.min_x, self.max_x - epsilon),
63 position.y,
64 clamp_f64(position.z, self.min_z, self.max_z - epsilon),
65 )
66 }
67
68 #[must_use]
69 pub(crate) fn is_within_bounds(self, bounding_box: WorldAabb) -> bool {
70 let epsilon = f64::from(1.0E-5_f32);
71 self.is_within_bounds_with_margin(bounding_box.min_x(), bounding_box.min_z(), 0.0)
72 && self.is_within_bounds_with_margin(
73 bounding_box.max_x() - epsilon,
74 bounding_box.max_z() - epsilon,
75 0.0,
76 )
77 }
78
79 #[must_use]
80 pub(crate) fn distance_to_border(self, x: f64, z: f64) -> f64 {
81 let from_north = z - self.min_z;
82 let from_south = self.max_z - z;
83 let from_west = x - self.min_x;
84 let from_east = self.max_x - x;
85 from_west.min(from_east).min(from_north).min(from_south)
86 }
87
88 #[must_use]
89 pub(crate) fn is_inside_close_to_border(self, x: f64, z: f64, bounding_box: WorldAabb) -> bool {
90 let box_max = bounding_box
91 .width()
92 .abs()
93 .max(bounding_box.depth().abs())
94 .max(1.0);
95 self.distance_to_border(x, z) < box_max * 2.0
96 && self.is_within_bounds_with_margin(x, z, box_max)
97 }
98
99 #[must_use]
100 pub(crate) fn outside_damage_amount(
101 self,
102 x: f64,
103 z: f64,
104 bounding_box: WorldAabb,
105 ) -> Option<f32> {
106 if self.is_within_bounds(bounding_box) {
107 return None;
108 }
109
110 let distance = self.distance_to_border(x, z) + self.safe_zone;
111 if distance >= 0.0 || self.damage_per_block <= 0.0 {
112 return None;
113 }
114
115 Some((-distance * self.damage_per_block).floor().max(1.0) as f32)
116 }
117
118 #[must_use]
119 pub(crate) fn collision_shapes_for(self, aabb: WorldAabb) -> Vec<WorldAabb> {
120 let min_x = self.min_x.floor();
121 let min_z = self.min_z.floor();
122 let max_x = self.max_x.ceil();
123 let max_z = self.max_z.ceil();
124 let shapes = [
125 WorldAabb::new(
126 f64::NEG_INFINITY,
127 f64::NEG_INFINITY,
128 f64::NEG_INFINITY,
129 min_x,
130 f64::INFINITY,
131 f64::INFINITY,
132 ),
133 WorldAabb::new(
134 max_x,
135 f64::NEG_INFINITY,
136 f64::NEG_INFINITY,
137 f64::INFINITY,
138 f64::INFINITY,
139 f64::INFINITY,
140 ),
141 WorldAabb::new(
142 f64::NEG_INFINITY,
143 f64::NEG_INFINITY,
144 f64::NEG_INFINITY,
145 f64::INFINITY,
146 f64::INFINITY,
147 min_z,
148 ),
149 WorldAabb::new(
150 f64::NEG_INFINITY,
151 f64::NEG_INFINITY,
152 max_z,
153 f64::INFINITY,
154 f64::INFINITY,
155 f64::INFINITY,
156 ),
157 ];
158
159 shapes
160 .into_iter()
161 .filter(|shape| shape.intersects(aabb))
162 .collect()
163 }
164}
165
166#[derive(Debug, Clone, PartialEq)]
168pub enum WorldBorderError {
169 NonFinite(&'static str, f64),
171 CenterOutOfRange(&'static str, f64),
173 NegativeLerpTime(i64),
175}
176
177impl fmt::Display for WorldBorderError {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 match *self {
180 Self::NonFinite(field, value) => {
181 write!(f, "world border {field} must be finite, got {value}")
182 }
183 Self::CenterOutOfRange(field, value) => write!(
184 f,
185 "world border {field} must be within +/-{MAX_CENTER_COORDINATE}, got {value}"
186 ),
187 Self::NegativeLerpTime(value) => {
188 write!(
189 f,
190 "world border lerp time must be non-negative, got {value}"
191 )
192 }
193 }
194 }
195}
196
197impl Error for WorldBorderError {}
198
199#[derive(Debug, Clone, Copy, PartialEq)]
200enum BorderExtent {
201 Static {
202 size: f64,
203 },
204 Moving {
205 from: f64,
206 to: f64,
207 lerp_duration: i64,
208 lerp_progress: i64,
209 size: f64,
210 previous_size: f64,
211 },
212}
213
214impl BorderExtent {
215 const fn static_size(size: f64) -> Self {
216 Self::Static { size }
217 }
218
219 #[expect(
220 clippy::float_cmp,
221 reason = "Vanilla collapses lerps when start and target sizes are exactly equal."
222 )]
223 fn moving(from: f64, to: f64, ticks: i64) -> Result<Self, WorldBorderError> {
224 if ticks < 0 {
225 return Err(WorldBorderError::NegativeLerpTime(ticks));
226 }
227 if ticks == 0 || from == to {
228 return Ok(Self::static_size(to));
229 }
230
231 Ok(Self::Moving {
232 from,
233 to,
234 lerp_duration: ticks,
235 lerp_progress: ticks,
236 size: from,
237 previous_size: from,
238 })
239 }
240
241 const fn size(self) -> f64 {
242 match self {
243 Self::Static { size } | Self::Moving { size, .. } => size,
244 }
245 }
246
247 const fn lerp_time(self) -> i64 {
248 match self {
249 Self::Static { .. } => 0,
250 Self::Moving { lerp_progress, .. } => lerp_progress,
251 }
252 }
253
254 const fn lerp_target(self) -> f64 {
255 match self {
256 Self::Static { size } => size,
257 Self::Moving { to, .. } => to,
258 }
259 }
260
261 fn status(self) -> BorderStatus {
262 match self {
263 Self::Static { .. } => BorderStatus::Stationary,
264 Self::Moving { from, to, .. } if to < from => BorderStatus::Shrinking,
265 Self::Moving { .. } => BorderStatus::Growing,
266 }
267 }
268
269 fn tick(self) -> Self {
270 let Self::Moving {
271 from,
272 to,
273 lerp_duration,
274 lerp_progress,
275 size,
276 ..
277 } = self
278 else {
279 return self;
280 };
281
282 let next_progress = lerp_progress - 1;
283 if next_progress <= 0 {
284 return Self::Static { size: to };
285 }
286
287 let progress = (lerp_duration - next_progress) as f64 / lerp_duration as f64;
288 Self::Moving {
289 from,
290 to,
291 lerp_duration,
292 lerp_progress: next_progress,
293 size: from + (to - from) * progress,
294 previous_size: size,
295 }
296 }
297}
298
299#[derive(Debug, Clone, PartialEq)]
300pub(crate) struct WorldBorder {
301 center_x: f64,
302 center_z: f64,
303 damage_per_block: f64,
304 safe_zone: f64,
305 warning_time: i32,
306 warning_blocks: i32,
307 absolute_max_size: i32,
308 extent: BorderExtent,
309}
310
311impl WorldBorder {
312 pub(crate) fn new(data: WorldBorderData) -> Result<Self, WorldBorderError> {
313 validate_center("center_x", data.center_x)?;
314 validate_center("center_z", data.center_z)?;
315 validate_finite("damage_per_block", data.damage_per_block)?;
316 validate_finite("safe_zone", data.safe_zone)?;
317 validate_finite("size", data.size)?;
318 validate_finite("lerp_target", data.lerp_target)?;
319
320 let extent = if data.lerp_time > 0 {
321 BorderExtent::moving(data.size, data.lerp_target, data.lerp_time)?
322 } else {
323 BorderExtent::static_size(data.size)
324 };
325
326 Ok(Self {
327 center_x: data.center_x,
328 center_z: data.center_z,
329 damage_per_block: data.damage_per_block,
330 safe_zone: data.safe_zone,
331 warning_time: data.warning_time,
332 warning_blocks: data.warning_blocks,
333 absolute_max_size: DEFAULT_ABSOLUTE_MAX_SIZE,
334 extent,
335 })
336 }
337
338 #[must_use]
339 pub(crate) fn snapshot(&self) -> WorldBorderSnapshot {
340 let old_size = self.extent.size();
341 let new_size = self.extent.lerp_target();
342 let half_size = old_size / 2.0;
343 let absolute_max = f64::from(self.absolute_max_size);
344 WorldBorderSnapshot {
345 center_x: self.center_x,
346 center_z: self.center_z,
347 old_size,
348 new_size,
349 lerp_time: self.extent.lerp_time(),
350 absolute_max_size: self.absolute_max_size,
351 warning_blocks: self.warning_blocks,
352 warning_time: self.warning_time,
353 damage_per_block: self.damage_per_block,
354 safe_zone: self.safe_zone,
355 min_x: clamp_f64(self.center_x - half_size, -absolute_max, absolute_max),
356 min_z: clamp_f64(self.center_z - half_size, -absolute_max, absolute_max),
357 max_x: clamp_f64(self.center_x + half_size, -absolute_max, absolute_max),
358 max_z: clamp_f64(self.center_z + half_size, -absolute_max, absolute_max),
359 status: self.extent.status(),
360 }
361 }
362
363 #[must_use]
364 pub(crate) const fn to_data(&self) -> WorldBorderData {
365 WorldBorderData {
366 center_x: self.center_x,
367 center_z: self.center_z,
368 damage_per_block: self.damage_per_block,
369 safe_zone: self.safe_zone,
370 warning_blocks: self.warning_blocks,
371 warning_time: self.warning_time,
372 size: self.extent.size(),
373 lerp_time: self.extent.lerp_time(),
374 lerp_target: self.extent.lerp_target(),
375 }
376 }
377
378 pub(crate) fn tick(&mut self) {
379 self.extent = self.extent.tick();
380 }
381
382 pub(crate) fn set_center(&mut self, x: f64, z: f64) -> Result<(), WorldBorderError> {
383 validate_center("center_x", x)?;
384 validate_center("center_z", z)?;
385 self.center_x = x;
386 self.center_z = z;
387 Ok(())
388 }
389
390 pub(crate) fn set_size(&mut self, size: f64) -> Result<(), WorldBorderError> {
391 validate_finite("size", size)?;
392 self.extent = BorderExtent::static_size(size);
393 Ok(())
394 }
395
396 pub(crate) fn lerp_size_between(
397 &mut self,
398 from: f64,
399 to: f64,
400 ticks: i64,
401 ) -> Result<(), WorldBorderError> {
402 validate_finite("from", from)?;
403 validate_finite("to", to)?;
404 self.extent = BorderExtent::moving(from, to, ticks)?;
405 Ok(())
406 }
407
408 pub(crate) const fn set_warning_time(&mut self, warning_time: i32) {
409 self.warning_time = warning_time;
410 }
411
412 pub(crate) const fn set_warning_blocks(&mut self, warning_blocks: i32) {
413 self.warning_blocks = warning_blocks;
414 }
415
416 pub(crate) fn set_damage_per_block(
417 &mut self,
418 damage_per_block: f64,
419 ) -> Result<(), WorldBorderError> {
420 validate_finite("damage_per_block", damage_per_block)?;
421 self.damage_per_block = damage_per_block;
422 Ok(())
423 }
424
425 pub(crate) fn set_safe_zone(&mut self, safe_zone: f64) -> Result<(), WorldBorderError> {
426 validate_finite("safe_zone", safe_zone)?;
427 self.safe_zone = safe_zone;
428 Ok(())
429 }
430}
431
432fn validate_center(field: &'static str, value: f64) -> Result<(), WorldBorderError> {
433 validate_finite(field, value)?;
434 if !(-MAX_CENTER_COORDINATE..=MAX_CENTER_COORDINATE).contains(&value) {
435 return Err(WorldBorderError::CenterOutOfRange(field, value));
436 }
437 Ok(())
438}
439
440const fn validate_finite(field: &'static str, value: f64) -> Result<(), WorldBorderError> {
441 if value.is_finite() {
442 Ok(())
443 } else {
444 Err(WorldBorderError::NonFinite(field, value))
445 }
446}
447
448fn clamp_f64(value: f64, min: f64, max: f64) -> f64 {
449 if value < min {
450 min
451 } else if value > max {
452 max
453 } else {
454 value
455 }
456}
457
458use super::{
459 CInitializeBorder, CSetBorderCenter, CSetBorderLerpSize, CSetBorderSize,
460 CSetBorderWarningDelay, CSetBorderWarningDistance, HeightmapType, RespawnData, World,
461 initialize_border_packet,
462};
463
464impl World {
465 #[must_use]
466 pub(crate) fn world_border_snapshot(&self) -> WorldBorderSnapshot {
467 self.world_border.lock().snapshot()
468 }
469
470 #[must_use]
472 pub fn is_block_within_world_border(&self, pos: BlockPos) -> bool {
473 self.world_border_snapshot().is_block_within_bounds(pos)
474 }
475
476 #[must_use]
478 pub fn clamp_to_world_border(&self, x: f64, y: f64, z: f64) -> BlockPos {
479 self.world_border_snapshot().clamp_to_bounds(x, y, z)
480 }
481
482 #[must_use]
483 pub(crate) fn initialize_border_packet(&self) -> CInitializeBorder {
484 initialize_border_packet(self.world_border_snapshot())
485 }
486
487 #[must_use]
488 pub(crate) fn world_border_adjusted_respawn_data(
489 &self,
490 respawn_data: RespawnData,
491 ) -> RespawnData {
492 let pos = respawn_data.pos();
493 if self.is_block_within_world_border(pos) {
494 return respawn_data;
495 }
496
497 let border = self.world_border_snapshot();
498 let center_pos = BlockPos::containing(border.center_x, 0.0, border.center_z);
499 let new_pos = self.heightmap_pos(HeightmapType::MotionBlocking, center_pos);
500 RespawnData::of(
501 respawn_data.dimension().clone(),
502 new_pos,
503 respawn_data.yaw,
504 respawn_data.pitch,
505 )
506 }
507
508 pub fn set_world_border_center(&self, x: f64, z: f64) -> Result<(), WorldBorderError> {
510 let (snapshot, data) = {
511 let mut border = self.world_border.lock();
512 border.set_center(x, z)?;
513 (border.snapshot(), border.to_data())
514 };
515 self.store_world_border_data_if_changed(data);
516 self.broadcast_to_all(CSetBorderCenter {
517 new_center_x: snapshot.center_x,
518 new_center_z: snapshot.center_z,
519 });
520 Ok(())
521 }
522
523 pub fn set_world_border_size(&self, size: f64) -> Result<(), WorldBorderError> {
525 let (snapshot, data) = {
526 let mut border = self.world_border.lock();
527 border.set_size(size)?;
528 (border.snapshot(), border.to_data())
529 };
530 self.store_world_border_data_if_changed(data);
531 self.broadcast_to_all(CSetBorderSize {
532 size: snapshot.new_size,
533 });
534 Ok(())
535 }
536
537 pub fn lerp_world_border_size_between(
539 &self,
540 from: f64,
541 to: f64,
542 ticks: i64,
543 ) -> Result<(), WorldBorderError> {
544 let (snapshot, data) = {
545 let mut border = self.world_border.lock();
546 border.lerp_size_between(from, to, ticks)?;
547 (border.snapshot(), border.to_data())
548 };
549 self.store_world_border_data_if_changed(data);
550 self.broadcast_to_all(CSetBorderLerpSize {
551 old_size: snapshot.old_size,
552 new_size: snapshot.new_size,
553 lerp_time: snapshot.lerp_time,
554 });
555 Ok(())
556 }
557
558 pub fn set_world_border_warning_time(&self, warning_time: i32) {
560 let data = {
561 let mut border = self.world_border.lock();
562 border.set_warning_time(warning_time);
563 border.to_data()
564 };
565 self.store_world_border_data_if_changed(data);
566 self.broadcast_to_all(CSetBorderWarningDelay {
567 warning_delay: warning_time,
568 });
569 }
570
571 pub fn set_world_border_warning_blocks(&self, warning_blocks: i32) {
573 let data = {
574 let mut border = self.world_border.lock();
575 border.set_warning_blocks(warning_blocks);
576 border.to_data()
577 };
578 self.store_world_border_data_if_changed(data);
579 self.broadcast_to_all(CSetBorderWarningDistance { warning_blocks });
580 }
581
582 pub fn set_world_border_damage_per_block(
584 &self,
585 damage_per_block: f64,
586 ) -> Result<(), WorldBorderError> {
587 let data = {
588 let mut border = self.world_border.lock();
589 border.set_damage_per_block(damage_per_block)?;
590 border.to_data()
591 };
592 self.store_world_border_data_if_changed(data);
593 Ok(())
594 }
595
596 pub fn set_world_border_safe_zone(&self, safe_zone: f64) -> Result<(), WorldBorderError> {
598 let data = {
599 let mut border = self.world_border.lock();
600 border.set_safe_zone(safe_zone)?;
601 border.to_data()
602 };
603 self.store_world_border_data_if_changed(data);
604 Ok(())
605 }
606
607 pub(super) fn tick_world_border(&self) {
608 let data = {
609 let mut border = self.world_border.lock();
610 border.tick();
611 border.to_data()
612 };
613 self.store_world_border_data_if_changed(data);
614 }
615
616 pub(super) fn sync_world_border_to_level_data(&self) {
617 let data = self.world_border.lock().to_data();
618 self.store_world_border_data_if_changed(data);
619 }
620
621 pub(super) fn store_world_border_data_if_changed(&self, data: WorldBorderData) {
622 let mut level_data = self.level_data.write();
623 if level_data.data().world_border != data {
624 level_data.data_mut().world_border = data;
625 }
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632
633 fn assert_f64_eq(actual: f64, expected: f64) {
634 assert!(
635 (actual - expected).abs() <= f64::EPSILON,
636 "expected {expected}, got {actual}"
637 );
638 }
639
640 fn assert_f32_eq(actual: f32, expected: f32) {
641 assert!(
642 (actual - expected).abs() <= f32::EPSILON,
643 "expected {expected}, got {actual}"
644 );
645 }
646
647 fn default_border() -> WorldBorder {
648 WorldBorder::new(WorldBorderData::default()).expect("default world border should load")
649 }
650
651 #[test]
652 fn default_border_matches_vanilla_initial_settings() {
653 let border = default_border();
654 let snapshot = border.snapshot();
655
656 assert_f64_eq(snapshot.center_x, 0.0);
657 assert_f64_eq(snapshot.center_z, 0.0);
658 assert_f64_eq(snapshot.old_size, WorldBorderData::default().size);
659 assert_f64_eq(snapshot.new_size, WorldBorderData::default().size);
660 assert_eq!(snapshot.lerp_time, 0);
661 assert_eq!(snapshot.warning_blocks, 5);
662 assert_eq!(snapshot.warning_time, 300);
663 assert_eq!(snapshot.absolute_max_size, DEFAULT_ABSOLUTE_MAX_SIZE);
664 }
665
666 #[test]
667 fn border_bounds_are_clamped_to_absolute_max_size() {
668 let mut border = default_border();
669 border
670 .set_center(MAX_CENTER_COORDINATE, MAX_CENTER_COORDINATE)
671 .expect("valid vanilla border center");
672 let snapshot = border.snapshot();
673
674 assert_f64_eq(snapshot.max_x, f64::from(DEFAULT_ABSOLUTE_MAX_SIZE));
675 assert_f64_eq(snapshot.max_z, f64::from(DEFAULT_ABSOLUTE_MAX_SIZE));
676 }
677
678 #[test]
679 fn block_position_bounds_use_vanilla_inclusive_min_exclusive_max() {
680 let mut border = default_border();
681 border.set_size(10.0).expect("valid border size");
682 let snapshot = border.snapshot();
683
684 assert!(snapshot.is_block_within_bounds(BlockPos::new(-5, 64, -5)));
685 assert!(snapshot.is_block_within_bounds(BlockPos::new(4, 64, 4)));
686 assert!(!snapshot.is_block_within_bounds(BlockPos::new(5, 64, 0)));
687 assert!(!snapshot.is_block_within_bounds(BlockPos::new(0, 64, 5)));
688 }
689
690 #[test]
691 fn clamp_to_bounds_matches_vanilla_blockpos_containing() {
692 let mut border = default_border();
693 border.set_size(10.0).expect("valid border size");
694 let snapshot = border.snapshot();
695
696 assert_eq!(
697 snapshot.clamp_to_bounds(12.7, 64.9, 5.5),
698 BlockPos::new(4, 64, 4)
699 );
700 assert_eq!(
701 snapshot.clamp_to_bounds(-8.1, -1.2, -9.3),
702 BlockPos::new(-5, -2, -5)
703 );
704 }
705
706 #[test]
707 fn moving_border_ticks_to_target_size() {
708 let mut border = default_border();
709 border
710 .lerp_size_between(10.0, 20.0, 2)
711 .expect("valid border lerp");
712
713 border.tick();
714 let first_tick = border.snapshot();
715 assert_f64_eq(first_tick.old_size, 15.0);
716 assert_f64_eq(first_tick.new_size, 20.0);
717 assert_eq!(first_tick.lerp_time, 1);
718 assert_eq!(first_tick.status, BorderStatus::Growing);
719
720 border.tick();
721 let second_tick = border.snapshot();
722 assert_f64_eq(second_tick.old_size, 20.0);
723 assert_f64_eq(second_tick.new_size, 20.0);
724 assert_eq!(second_tick.lerp_time, 0);
725 assert_eq!(second_tick.status, BorderStatus::Stationary);
726 }
727
728 #[test]
729 fn close_to_border_matches_vanilla_distance_gate() {
730 let mut border = default_border();
731 border.set_size(10.0).expect("valid border size");
732 let snapshot = border.snapshot();
733 let aabb = WorldAabb::new(3.8, 0.0, -0.3, 4.4, 1.8, 0.3);
734
735 assert!(snapshot.is_inside_close_to_border(4.1, 0.0, aabb));
736 assert!(!snapshot.is_inside_close_to_border(0.0, 0.0, aabb));
737 }
738
739 #[test]
740 fn collision_shape_represents_outside_of_border() {
741 let mut border = default_border();
742 border.set_size(10.0).expect("valid border size");
743 let snapshot = border.snapshot();
744 let crossing_east = WorldAabb::new(4.5, 0.0, -0.3, 5.2, 1.8, 0.3);
745 let inside = WorldAabb::new(0.0, 0.0, -0.3, 0.6, 1.8, 0.3);
746
747 assert_eq!(snapshot.collision_shapes_for(crossing_east).len(), 1);
748 assert!(snapshot.collision_shapes_for(inside).is_empty());
749 }
750
751 #[test]
752 fn outside_damage_is_ignored_inside_safe_zone() {
753 let mut border = default_border();
754 border.set_size(10.0).expect("valid border size");
755 let snapshot = border.snapshot();
756 let just_outside = WorldAabb::entity_box(5.2, 0.0, 0.0, 0.3, 1.8);
757
758 assert!(
759 snapshot
760 .outside_damage_amount(5.2, 0.0, just_outside)
761 .is_none()
762 );
763 }
764
765 #[test]
766 fn outside_damage_uses_vanilla_minimum_damage() {
767 let mut border = default_border();
768 border.set_size(10.0).expect("valid border size");
769 let snapshot = border.snapshot();
770 let outside = WorldAabb::entity_box(12.0, 0.0, 0.0, 0.3, 1.8);
771
772 let damage = snapshot
773 .outside_damage_amount(12.0, 0.0, outside)
774 .expect("outside player should take border damage");
775
776 assert_f32_eq(damage, 1.0);
777 }
778
779 #[test]
780 fn outside_damage_uses_vanilla_floor_after_scaling() {
781 let border = WorldBorder::new(WorldBorderData {
782 size: 10.0,
783 safe_zone: 0.0,
784 damage_per_block: 2.0,
785 ..WorldBorderData::default()
786 })
787 .expect("valid world border");
788 let snapshot = border.snapshot();
789 let outside = WorldAabb::entity_box(8.4, 0.0, 0.0, 0.3, 1.8);
790
791 let damage = snapshot
792 .outside_damage_amount(8.4, 0.0, outside)
793 .expect("outside player should take border damage");
794
795 assert_f32_eq(damage, 6.0);
796 }
797
798 #[test]
799 fn outside_damage_is_disabled_when_damage_per_block_is_not_positive() {
800 let border = WorldBorder::new(WorldBorderData {
801 size: 10.0,
802 safe_zone: 0.0,
803 damage_per_block: 0.0,
804 ..WorldBorderData::default()
805 })
806 .expect("valid world border");
807 let snapshot = border.snapshot();
808 let outside = WorldAabb::entity_box(8.0, 0.0, 0.0, 0.3, 1.8);
809
810 assert!(snapshot.outside_damage_amount(8.0, 0.0, outside).is_none());
811 }
812}