flatland-presentation 0.2.23

Gfx sprite mode resolution — maps game states to sprite sheet mode ids
Documentation
//! Client-side facing expansion for directional sprite sheets (`walk_north`, …).

/// Movement modes that may have per-direction variants on a sprite sheet.
pub const PLAYER_DIRECTIONAL_BASE_MODES: &[&str] = &["idle", "walk", "run"];

/// 8-way facing labels (N/NE/E/SE/S/SW/W/NW) for paperdoll and directional sheets.
///
/// Camera yaw: 0 = north, π/2 = east (same as [`facing_yaw_from_axes`] in gfx).
pub fn cardinal_facing_label(yaw: f32) -> &'static str {
    eight_way_facing_label(yaw)
}

/// Bin continuous yaw into one of eight compass labels.
pub fn eight_way_facing_label(yaw: f32) -> &'static str {
    let deg = yaw.to_degrees().rem_euclid(360.0);
    // 0° north, 45° NE, … — each sector is ±22.5° around the compass point.
    if (22.5..67.5).contains(&deg) {
        "northeast"
    } else if (67.5..112.5).contains(&deg) {
        "east"
    } else if (112.5..157.5).contains(&deg) {
        "southeast"
    } else if (157.5..202.5).contains(&deg) {
        "south"
    } else if (202.5..247.5).contains(&deg) {
        "southwest"
    } else if (247.5..292.5).contains(&deg) {
        "west"
    } else if (292.5..337.5).contains(&deg) {
        "northwest"
    } else {
        "north"
    }
}

/// Legacy 4-way bin (N/E/S/W) — fallback when a sheet lacks 8-way cells.
pub fn four_way_facing_label(yaw: f32) -> &'static str {
    let deg = yaw.to_degrees().rem_euclid(360.0);
    if (45.0..135.0).contains(&deg) {
        "east"
    } else if (135.0..225.0).contains(&deg) {
        "south"
    } else if (225.0..315.0).contains(&deg) {
        "west"
    } else {
        "north"
    }
}

/// Strip a directional suffix so stale server modes like `walk_north` still face correctly.
/// Longer suffixes (`_northeast`) are matched before shorter ones (`_north`).
pub fn base_sprite_mode(mode: &str) -> &str {
    for suffix in [
        "_northeast",
        "_northwest",
        "_southeast",
        "_southwest",
        "_north",
        "_east",
        "_south",
        "_west",
    ] {
        if let Some(base) = mode.strip_suffix(suffix) {
            return base;
        }
    }
    mode
}

/// True when `sheet_modes` contains `mode` or a directional variant (`{mode}_north`, …).
pub fn sheet_has_mode(sheet_modes: &[String], mode: &str) -> bool {
    if sheet_modes.iter().any(|m| m == mode) {
        return true;
    }
    let prefix = format!("{mode}_");
    sheet_modes.iter().any(|m| m.starts_with(&prefix))
}

/// Expand a base gfx mode with client facing when the sheet has directional variants.
/// Prefers 8-way labels, then falls back to 4-way for older PNG sheets.
pub fn expand_directional_draw_mode(
    sheet_modes: &[String],
    base_mode: &str,
    facing_yaw: f32,
) -> String {
    let base = base_sprite_mode(base_mode);
    let eight = eight_way_facing_label(facing_yaw);
    let four = four_way_facing_label(facing_yaw);
    for label in [eight, four] {
        let directional = format!("{base}_{label}");
        if sheet_has_mode(sheet_modes, &directional) {
            return directional;
        }
    }
    if sheet_has_mode(sheet_modes, base) {
        return base.to_string();
    }
    // Paperdoll sheets always ship idle_* — fall back so combat/chase still draw.
    for label in [eight, four] {
        let idle_dir = format!("idle_{label}");
        if sheet_has_mode(sheet_modes, &idle_dir) {
            return idle_dir;
        }
    }
    if sheet_has_mode(sheet_modes, "idle") {
        return "idle".to_string();
    }
    base.to_string()
}

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

    fn modes(ids: &[&str]) -> Vec<String> {
        ids.iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn eight_way_facing_label_bins() {
        assert_eq!(eight_way_facing_label(0.0), "north");
        assert_eq!(
            eight_way_facing_label(std::f32::consts::FRAC_PI_4),
            "northeast"
        );
        assert_eq!(
            eight_way_facing_label(std::f32::consts::FRAC_PI_2),
            "east"
        );
        assert_eq!(eight_way_facing_label(std::f32::consts::PI), "south");
        assert_eq!(
            eight_way_facing_label(-std::f32::consts::FRAC_PI_4),
            "northwest"
        );
    }

    #[test]
    fn cardinal_facing_label_bins() {
        assert_eq!(cardinal_facing_label(0.0), "north");
        assert_eq!(cardinal_facing_label(std::f32::consts::FRAC_PI_2), "east");
        assert_eq!(cardinal_facing_label(std::f32::consts::PI), "south");
    }

    #[test]
    fn base_sprite_mode_strips_suffix() {
        assert_eq!(base_sprite_mode("walk_north"), "walk");
        assert_eq!(base_sprite_mode("walk_northeast"), "walk");
        assert_eq!(base_sprite_mode("idle"), "idle");
    }

    #[test]
    fn expand_directional_when_sheet_has_variant() {
        let sheet = modes(&[
            "idle_north",
            "walk_north",
            "walk_northeast",
            "walk_east",
            "combat",
        ]);
        assert_eq!(
            expand_directional_draw_mode(&sheet, "walk", 0.0),
            "walk_north"
        );
        assert_eq!(
            expand_directional_draw_mode(&sheet, "walk", std::f32::consts::FRAC_PI_4),
            "walk_northeast"
        );
        assert_eq!(
            expand_directional_draw_mode(&sheet, "walk", std::f32::consts::FRAC_PI_2),
            "walk_east"
        );
        assert_eq!(expand_directional_draw_mode(&sheet, "combat", 0.0), "combat");
    }

    #[test]
    fn expand_falls_back_to_four_way_when_eight_missing() {
        let sheet = modes(&["walk_north", "walk_east", "walk_south", "walk_west"]);
        // 45° is NE in 8-way; 4-way bins it to east.
        assert_eq!(
            expand_directional_draw_mode(&sheet, "walk", std::f32::consts::FRAC_PI_4),
            "walk_east"
        );
    }

    #[test]
    fn expand_falls_back_to_base_without_directional() {
        let sheet = modes(&["walk", "idle"]);
        assert_eq!(expand_directional_draw_mode(&sheet, "walk", 0.0), "walk");
    }
}