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