1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
use super::{Position, HALF_WORLD_SIZE};

impl Position {
    /// Returns this position's horizontal "world coordinate".
    ///
    /// The value is equal to `50 * room_x + x`, where `room_x` is defined as
    /// `room_x = -xx - 1` for `Wxx` rooms and as `room_x = xx` for `Exx` rooms.
    #[inline]
    pub fn world_x(self) -> i32 {
        self.room_x() * 50 + (self.x() as i32)
    }

    /// Returns this position's vertical "world coordinate".
    ///
    /// The value is equal to `50 * room_y + y`, where `room_y` is defined as
    /// `room_y = -yy - 1` for `Nyy` rooms and as `room_y = yy` for `Syy` rooms.
    #[inline]
    pub fn world_y(self) -> i32 {
        self.room_y() * 50 + (self.y() as i32)
    }

    /// Returns this position's "world coordinates".
    ///
    /// The first value is equal to `50 * room_x + x`, where `room_x` is defined
    /// as `room_x = -xx - 1` for `Wxx` rooms and as `room_x = xx` for `Exx`
    /// rooms.
    ///
    /// The second value is equal to `50 * room_y + y`, where `room_y` is
    /// defined as `room_y = -yy - 1` for `Nyy` rooms and as `room_y = yy`
    /// for `Syy` rooms.
    ///
    /// See also [`Position::world_x`] and
    /// [`Position::world_y`].
    #[inline]
    pub fn world_coords(self) -> (i32, i32) {
        (self.world_x(), self.world_y())
    }

    /// Creates a room position from world coords.
    ///
    /// # Panics
    ///
    /// Panics if either x or y is out of the range `-128 * 50 .. +128 * 50`.
    ///
    /// See [`Position::world_coords`].
    #[inline]
    pub fn from_world_coords(x: i32, y: i32) -> Self {
        // not inclusive since the range for world coords in `-128..=127`, and the range
        // for room coords is `0..=49`.
        assert!(
            (-HALF_WORLD_SIZE * 50..HALF_WORLD_SIZE * 50).contains(&x),
            "out of bounds world x: {}",
            x
        );
        assert!(
            (-HALF_WORLD_SIZE * 50..HALF_WORLD_SIZE * 50).contains(&y),
            "out of bounds world y: {}",
            y
        );

        // We do the `HALF_WORLD_SIZE` transition here first so that the division and
        // modulo operations work correctly.
        let pos_x = (x + HALF_WORLD_SIZE * 50) as u32;
        let pos_y = (y + HALF_WORLD_SIZE * 50) as u32;
        let room_x = pos_x / 50;
        let room_y = pos_y / 50;
        let x = pos_x % 50;
        let y = pos_y % 50;

        Self::from_coords_and_world_coords_adjusted(x, y, room_x, room_y)
    }
}

#[cfg(test)]
mod test {
    use super::Position;

    const TEST_ROOM_NAMES: &[&str] = &[
        "E1N1", "E20N0", "W0N0", "E0N0", "W0S0", "E0S0", "W0N0", "E0N0", "W0S0", "E0S0", "W50S20",
        "W127S127", "W127N127", "E127S127", "E127N127",
    ];
    const TEST_COORDS: &[u32] = &[0, 21, 44, 49];

    #[test]
    fn world_coords_round_trip() {
        for room_name in TEST_ROOM_NAMES {
            for x in TEST_COORDS.iter().cloned() {
                for y in TEST_COORDS.iter().cloned() {
                    let original_pos = Position::new(x, y, room_name.parse().unwrap());
                    let (wx, wy) = original_pos.world_coords();
                    let new = Position::from_world_coords(wx, wy);
                    assert_eq!(original_pos, new);
                }
            }
        }
    }
}