Skip to main content

flatland_presentation/
directional.rs

1//! Client-side facing expansion for directional sprite sheets (`walk_north`, …).
2
3/// Movement modes that may have per-direction variants on a sprite sheet.
4pub const PLAYER_DIRECTIONAL_BASE_MODES: &[&str] = &["idle", "walk", "run"];
5
6/// 8-way facing labels (N/NE/E/SE/S/SW/W/NW) for paperdoll and directional sheets.
7///
8/// Camera yaw: 0 = north, π/2 = east (same as [`facing_yaw_from_axes`] in gfx).
9pub fn cardinal_facing_label(yaw: f32) -> &'static str {
10    eight_way_facing_label(yaw)
11}
12
13/// Bin continuous yaw into one of eight compass labels.
14pub fn eight_way_facing_label(yaw: f32) -> &'static str {
15    let deg = yaw.to_degrees().rem_euclid(360.0);
16    // 0° north, 45° NE, … — each sector is ±22.5° around the compass point.
17    if (22.5..67.5).contains(&deg) {
18        "northeast"
19    } else if (67.5..112.5).contains(&deg) {
20        "east"
21    } else if (112.5..157.5).contains(&deg) {
22        "southeast"
23    } else if (157.5..202.5).contains(&deg) {
24        "south"
25    } else if (202.5..247.5).contains(&deg) {
26        "southwest"
27    } else if (247.5..292.5).contains(&deg) {
28        "west"
29    } else if (292.5..337.5).contains(&deg) {
30        "northwest"
31    } else {
32        "north"
33    }
34}
35
36/// Legacy 4-way bin (N/E/S/W) — fallback when a sheet lacks 8-way cells.
37pub fn four_way_facing_label(yaw: f32) -> &'static str {
38    let deg = yaw.to_degrees().rem_euclid(360.0);
39    if (45.0..135.0).contains(&deg) {
40        "east"
41    } else if (135.0..225.0).contains(&deg) {
42        "south"
43    } else if (225.0..315.0).contains(&deg) {
44        "west"
45    } else {
46        "north"
47    }
48}
49
50/// Strip a directional suffix so stale server modes like `walk_north` still face correctly.
51/// Longer suffixes (`_northeast`) are matched before shorter ones (`_north`).
52pub fn base_sprite_mode(mode: &str) -> &str {
53    for suffix in [
54        "_northeast",
55        "_northwest",
56        "_southeast",
57        "_southwest",
58        "_north",
59        "_east",
60        "_south",
61        "_west",
62    ] {
63        if let Some(base) = mode.strip_suffix(suffix) {
64            return base;
65        }
66    }
67    mode
68}
69
70/// True when `sheet_modes` contains `mode` or a directional variant (`{mode}_north`, …).
71pub fn sheet_has_mode(sheet_modes: &[String], mode: &str) -> bool {
72    if sheet_modes.iter().any(|m| m == mode) {
73        return true;
74    }
75    let prefix = format!("{mode}_");
76    sheet_modes.iter().any(|m| m.starts_with(&prefix))
77}
78
79/// Expand a base gfx mode with client facing when the sheet has directional variants.
80/// Prefers 8-way labels, then falls back to 4-way for older PNG sheets.
81pub fn expand_directional_draw_mode(
82    sheet_modes: &[String],
83    base_mode: &str,
84    facing_yaw: f32,
85) -> String {
86    let base = base_sprite_mode(base_mode);
87    let eight = eight_way_facing_label(facing_yaw);
88    let four = four_way_facing_label(facing_yaw);
89    for label in [eight, four] {
90        let directional = format!("{base}_{label}");
91        if sheet_has_mode(sheet_modes, &directional) {
92            return directional;
93        }
94    }
95    if sheet_has_mode(sheet_modes, base) {
96        return base.to_string();
97    }
98    // Paperdoll sheets always ship idle_* — fall back so combat/chase still draw.
99    for label in [eight, four] {
100        let idle_dir = format!("idle_{label}");
101        if sheet_has_mode(sheet_modes, &idle_dir) {
102            return idle_dir;
103        }
104    }
105    if sheet_has_mode(sheet_modes, "idle") {
106        return "idle".to_string();
107    }
108    base.to_string()
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    fn modes(ids: &[&str]) -> Vec<String> {
116        ids.iter().map(|s| s.to_string()).collect()
117    }
118
119    #[test]
120    fn eight_way_facing_label_bins() {
121        assert_eq!(eight_way_facing_label(0.0), "north");
122        assert_eq!(
123            eight_way_facing_label(std::f32::consts::FRAC_PI_4),
124            "northeast"
125        );
126        assert_eq!(
127            eight_way_facing_label(std::f32::consts::FRAC_PI_2),
128            "east"
129        );
130        assert_eq!(eight_way_facing_label(std::f32::consts::PI), "south");
131        assert_eq!(
132            eight_way_facing_label(-std::f32::consts::FRAC_PI_4),
133            "northwest"
134        );
135    }
136
137    #[test]
138    fn cardinal_facing_label_bins() {
139        assert_eq!(cardinal_facing_label(0.0), "north");
140        assert_eq!(cardinal_facing_label(std::f32::consts::FRAC_PI_2), "east");
141        assert_eq!(cardinal_facing_label(std::f32::consts::PI), "south");
142    }
143
144    #[test]
145    fn base_sprite_mode_strips_suffix() {
146        assert_eq!(base_sprite_mode("walk_north"), "walk");
147        assert_eq!(base_sprite_mode("walk_northeast"), "walk");
148        assert_eq!(base_sprite_mode("idle"), "idle");
149    }
150
151    #[test]
152    fn expand_directional_when_sheet_has_variant() {
153        let sheet = modes(&[
154            "idle_north",
155            "walk_north",
156            "walk_northeast",
157            "walk_east",
158            "combat",
159        ]);
160        assert_eq!(
161            expand_directional_draw_mode(&sheet, "walk", 0.0),
162            "walk_north"
163        );
164        assert_eq!(
165            expand_directional_draw_mode(&sheet, "walk", std::f32::consts::FRAC_PI_4),
166            "walk_northeast"
167        );
168        assert_eq!(
169            expand_directional_draw_mode(&sheet, "walk", std::f32::consts::FRAC_PI_2),
170            "walk_east"
171        );
172        assert_eq!(expand_directional_draw_mode(&sheet, "combat", 0.0), "combat");
173    }
174
175    #[test]
176    fn expand_falls_back_to_four_way_when_eight_missing() {
177        let sheet = modes(&["walk_north", "walk_east", "walk_south", "walk_west"]);
178        // 45° is NE in 8-way; 4-way bins it to east.
179        assert_eq!(
180            expand_directional_draw_mode(&sheet, "walk", std::f32::consts::FRAC_PI_4),
181            "walk_east"
182        );
183    }
184
185    #[test]
186    fn expand_falls_back_to_base_without_directional() {
187        let sheet = modes(&["walk", "idle"]);
188        assert_eq!(expand_directional_draw_mode(&sheet, "walk", 0.0), "walk");
189    }
190}