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
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/// Invalid world border state or update.
167#[derive(Debug, Clone, PartialEq)]
168pub enum WorldBorderError {
169    /// A floating-point field was NaN or infinite.
170    NonFinite(&'static str, f64),
171    /// A center coordinate exceeded vanilla's allowed range.
172    CenterOutOfRange(&'static str, f64),
173    /// A size lerp was requested with a negative duration.
174    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    /// Returns whether a block position is inside this world's vanilla world border.
471    #[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    /// Clamps a world-space position to this world's vanilla world border and floors it to a block position.
477    #[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    /// Sets the world border center and broadcasts the vanilla center update packet.
509    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    /// Sets a static world border size and broadcasts the vanilla size update packet.
524    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    /// Starts a vanilla world border size lerp and broadcasts the lerp update packet.
538    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    /// Sets the client warning time and broadcasts the vanilla warning-delay packet.
559    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    /// Sets the client warning distance and broadcasts the vanilla warning-distance packet.
572    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    /// Sets world border damage per block outside the safe zone.
583    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    /// Sets the safe distance outside the world border before damage starts.
597    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}