Skip to main content

steel_core/world/
border.rs

1//! World border state and collision helpers.
2
3use 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/// Invalid world border state or update.
168#[derive(Debug, Clone, PartialEq)]
169pub enum WorldBorderError {
170    /// A floating-point field was NaN or infinite.
171    NonFinite(&'static str, f64),
172    /// A center coordinate exceeded vanilla's allowed range.
173    CenterOutOfRange(&'static str, f64),
174    /// A size lerp was requested with a negative duration.
175    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    /// Returns whether a block position is inside this world's vanilla world border.
472    #[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    /// Clamps a world-space position to this world's vanilla world border and floors it to a block position.
478    #[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    /// Sets the world border center and broadcasts the vanilla center update packet.
510    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    /// Sets a static world border size and broadcasts the vanilla size update packet.
525    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    /// Starts a vanilla world border size lerp and broadcasts the lerp update packet.
539    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    /// Sets the client warning time and broadcasts the vanilla warning-delay packet.
560    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    /// Sets the client warning distance and broadcasts the vanilla warning-distance packet.
573    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    /// Sets world border damage per block outside the safe zone.
584    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    /// Sets the safe distance outside the world border before damage starts.
598    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}